mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-10 11:33:01 +00:00
flesh out oauth provider
- avoid logging credentials - implement some missing methods
This commit is contained in:
2
.flake8
2
.flake8
@@ -10,7 +10,7 @@
|
|||||||
# E402: module level import not at top of file
|
# E402: module level import not at top of file
|
||||||
# I100: Import statements are in the wrong order
|
# I100: Import statements are in the wrong order
|
||||||
# I101: Imported names are in the wrong order. Should be
|
# I101: Imported names are in the wrong order. Should be
|
||||||
ignore = E, C, W, F401, F403, F811, F841, E402, I100, I101
|
ignore = E, C, W, F401, F403, F811, F841, E402, I100, I101, D400
|
||||||
|
|
||||||
exclude =
|
exclude =
|
||||||
.cache,
|
.cache,
|
||||||
|
@@ -49,9 +49,10 @@ class JupyterHubRequestValidator(RequestValidator):
|
|||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
if oauth_client is None:
|
if oauth_client is None:
|
||||||
raise web.HTTPError(400, "Bad OAuth Client")
|
return False
|
||||||
if not compare_token(oauth_client.secret, client_secret):
|
if not compare_token(oauth_client.secret, client_secret):
|
||||||
raise web.HTTPError(400, "Bad OAuth Client")
|
app_log.warning("Client secret mismatch for %s", client_id)
|
||||||
|
return False
|
||||||
|
|
||||||
request.client = oauth_client
|
request.client = oauth_client
|
||||||
return True
|
return True
|
||||||
@@ -75,6 +76,7 @@ class JupyterHubRequestValidator(RequestValidator):
|
|||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
if orm_client is None:
|
if orm_client is None:
|
||||||
|
app_log.warning("No such oauth client %s", client_id)
|
||||||
return False
|
return False
|
||||||
request.client = orm_client
|
request.client = orm_client
|
||||||
return True
|
return True
|
||||||
@@ -96,8 +98,8 @@ class JupyterHubRequestValidator(RequestValidator):
|
|||||||
Method is used by:
|
Method is used by:
|
||||||
- Authorization Code Grant (during token request)
|
- Authorization Code Grant (during token request)
|
||||||
"""
|
"""
|
||||||
app_log.debug("confirm_redirect_uri: client_id=%s, code=%s, redirect_uri=%s",
|
app_log.debug("confirm_redirect_uri: client_id=%s, redirect_uri=%s",
|
||||||
client_id, code, redirect_uri,
|
client_id, redirect_uri,
|
||||||
)
|
)
|
||||||
orm_client = (
|
orm_client = (
|
||||||
self.db
|
self.db
|
||||||
@@ -106,9 +108,13 @@ class JupyterHubRequestValidator(RequestValidator):
|
|||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
if orm_client is None:
|
if orm_client is None:
|
||||||
|
app_log.warning("No such oauth client %s", client_id)
|
||||||
return False
|
return False
|
||||||
# TODO: confirm redirect uri
|
if redirect_uri == orm_client.redirect_uri:
|
||||||
return True
|
return True
|
||||||
|
else:
|
||||||
|
app_log.warning("Redirect uri %s != %s", redirect_uri, orm_client.redirect_uri)
|
||||||
|
return False
|
||||||
|
|
||||||
def get_default_redirect_uri(self, client_id, request, *args, **kwargs):
|
def get_default_redirect_uri(self, client_id, request, *args, **kwargs):
|
||||||
"""Get the default redirect URI for the client.
|
"""Get the default redirect URI for the client.
|
||||||
@@ -126,7 +132,7 @@ class JupyterHubRequestValidator(RequestValidator):
|
|||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
if orm_client is None:
|
if orm_client is None:
|
||||||
return False
|
raise KeyError(client_id)
|
||||||
return orm_client.redirect_uri
|
return orm_client.redirect_uri
|
||||||
|
|
||||||
def get_default_scopes(self, client_id, request, *args, **kwargs):
|
def get_default_scopes(self, client_id, request, *args, **kwargs):
|
||||||
@@ -150,8 +156,7 @@ class JupyterHubRequestValidator(RequestValidator):
|
|||||||
Method is used by:
|
Method is used by:
|
||||||
- Refresh token grant
|
- Refresh token grant
|
||||||
"""
|
"""
|
||||||
token = find_token()
|
raise NotImplementedError()
|
||||||
return token.scopes
|
|
||||||
|
|
||||||
def is_within_original_scope(self, request_scopes, refresh_token, request, *args, **kwargs):
|
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.
|
"""Check if requested scopes are within a scope of the refresh token.
|
||||||
@@ -169,8 +174,7 @@ class JupyterHubRequestValidator(RequestValidator):
|
|||||||
Method is used by:
|
Method is used by:
|
||||||
- Refresh token grant
|
- Refresh token grant
|
||||||
"""
|
"""
|
||||||
return set(request_scopes)
|
raise NotImplementedError()
|
||||||
return False
|
|
||||||
|
|
||||||
def invalidate_authorization_code(self, client_id, code, request, *args, **kwargs):
|
def invalidate_authorization_code(self, client_id, code, request, *args, **kwargs):
|
||||||
"""Invalidate an authorization code after use.
|
"""Invalidate an authorization code after use.
|
||||||
@@ -180,7 +184,7 @@ class JupyterHubRequestValidator(RequestValidator):
|
|||||||
Method is used by:
|
Method is used by:
|
||||||
- Authorization Code Grant
|
- Authorization Code Grant
|
||||||
"""
|
"""
|
||||||
app_log.debug("Deleting oauth code %s for %s", code, client_id)
|
app_log.debug("Deleting oauth code %s... for %s", code[:3], client_id)
|
||||||
orm_code = self.db.query(orm.OAuthCode).filter_by(code=code).first()
|
orm_code = self.db.query(orm.OAuthCode).filter_by(code=code).first()
|
||||||
if orm_code is not None:
|
if orm_code is not None:
|
||||||
self.db.delete(orm_code)
|
self.db.delete(orm_code)
|
||||||
@@ -221,7 +225,8 @@ class JupyterHubRequestValidator(RequestValidator):
|
|||||||
Method is used by:
|
Method is used by:
|
||||||
- Authorization Code Grant
|
- Authorization Code Grant
|
||||||
"""
|
"""
|
||||||
app_log.debug("Saving authorization code %s, %s, %s, %s", client_id, code, args, kwargs)
|
log_code = code.get('code', 'undefined')[:3] + '...'
|
||||||
|
app_log.debug("Saving authorization code %s, %s, %s, %s", client_id, log_code, args, kwargs)
|
||||||
orm_client = (
|
orm_client = (
|
||||||
self.db
|
self.db
|
||||||
.query(orm.OAuthClient)
|
.query(orm.OAuthClient)
|
||||||
@@ -236,6 +241,8 @@ class JupyterHubRequestValidator(RequestValidator):
|
|||||||
code=code['code'],
|
code=code['code'],
|
||||||
# oauth has 5 minutes to complete
|
# oauth has 5 minutes to complete
|
||||||
expires_at=int(datetime.utcnow().timestamp() + 300),
|
expires_at=int(datetime.utcnow().timestamp() + 300),
|
||||||
|
# TODO: persist oauth scopes
|
||||||
|
# scopes=request.scopes,
|
||||||
user=request.user.orm_user,
|
user=request.user.orm_user,
|
||||||
redirect_uri=orm_client.redirect_uri,
|
redirect_uri=orm_client.redirect_uri,
|
||||||
session_id=request.session_id,
|
session_id=request.session_id,
|
||||||
@@ -260,7 +267,7 @@ class JupyterHubRequestValidator(RequestValidator):
|
|||||||
Method is used by:
|
Method is used by:
|
||||||
- Authorization Token Grant Dispatcher
|
- Authorization Token Grant Dispatcher
|
||||||
"""
|
"""
|
||||||
return []
|
raise NotImplementedError("TODO")
|
||||||
|
|
||||||
def save_token(self, token, request, *args, **kwargs):
|
def save_token(self, token, request, *args, **kwargs):
|
||||||
"""Persist the token with a token type specific method.
|
"""Persist the token with a token type specific method.
|
||||||
@@ -307,7 +314,19 @@ class JupyterHubRequestValidator(RequestValidator):
|
|||||||
- Resource Owner Password Credentials Grant (might not associate a client)
|
- Resource Owner Password Credentials Grant (might not associate a client)
|
||||||
- Client Credentials grant
|
- Client Credentials grant
|
||||||
"""
|
"""
|
||||||
app_log.debug("Saving bearer token %s", token)
|
log_token = {}
|
||||||
|
log_token.update(token)
|
||||||
|
scopes = token['scope'].split(' ')
|
||||||
|
# TODO:
|
||||||
|
if scopes != ['identify']:
|
||||||
|
raise ValueError("Only 'identify' scope is supported")
|
||||||
|
# redact sensitive keys in log
|
||||||
|
for key in ('access_token', 'refresh_token', 'state'):
|
||||||
|
if key in token:
|
||||||
|
value = token[key]
|
||||||
|
if isinstance(value, str):
|
||||||
|
log_token[key] = 'REDACTED'
|
||||||
|
app_log.debug("Saving bearer token %s", log_token)
|
||||||
if request.user is None:
|
if request.user is None:
|
||||||
raise ValueError("No user for access token: %s" % request.user)
|
raise ValueError("No user for access token: %s" % request.user)
|
||||||
client = self.db.query(orm.OAuthClient).filter_by(identifier=request.client.client_id).first()
|
client = self.db.query(orm.OAuthClient).filter_by(identifier=request.client.client_id).first()
|
||||||
@@ -316,7 +335,8 @@ class JupyterHubRequestValidator(RequestValidator):
|
|||||||
grant_type=orm.GrantType.authorization_code,
|
grant_type=orm.GrantType.authorization_code,
|
||||||
expires_at=datetime.utcnow().timestamp() + token['expires_in'],
|
expires_at=datetime.utcnow().timestamp() + token['expires_in'],
|
||||||
refresh_token=token['refresh_token'],
|
refresh_token=token['refresh_token'],
|
||||||
# refresh_expires_at=access_token.refresh_expires_at,
|
# TODO: save scopes,
|
||||||
|
# scopes=scopes,
|
||||||
token=token['access_token'],
|
token=token['access_token'],
|
||||||
session_id=request.session_id,
|
session_id=request.session_id,
|
||||||
user=request.user,
|
user=request.user,
|
||||||
@@ -507,27 +527,6 @@ class JupyterHubRequestValidator(RequestValidator):
|
|||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
class HashComparable:
|
|
||||||
"""An object for storing hashed tokens
|
|
||||||
|
|
||||||
Overrides `==` so that it compares as equal to its unhashed original
|
|
||||||
|
|
||||||
Needed for storing hashed client_secrets
|
|
||||||
because python-oauth2 uses::
|
|
||||||
|
|
||||||
secret == client.client_secret
|
|
||||||
|
|
||||||
and we don't want to store unhashed secrets in the database.
|
|
||||||
"""
|
|
||||||
def __init__(self, hashed_token):
|
|
||||||
self.hashed_token = hashed_token
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "<{} '{}'>".format(self.__class__.__name__, self.hashed_token)
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
return compare_token(self.hashed_token, other)
|
|
||||||
|
|
||||||
|
|
||||||
class JupyterHubOAuthServer(WebApplicationServer):
|
class JupyterHubOAuthServer(WebApplicationServer):
|
||||||
def __init__(self, db, validator, *args, **kwargs):
|
def __init__(self, db, validator, *args, **kwargs):
|
||||||
|
@@ -469,6 +469,7 @@ class OAuthAccessToken(Hashed, Base):
|
|||||||
grant_type = Column(Enum(GrantType), nullable=False)
|
grant_type = Column(Enum(GrantType), nullable=False)
|
||||||
expires_at = Column(Integer)
|
expires_at = Column(Integer)
|
||||||
refresh_token = Column(Unicode(255))
|
refresh_token = Column(Unicode(255))
|
||||||
|
# TODO: drop refresh_expires_at. Refresh tokens shouldn't expire
|
||||||
refresh_expires_at = Column(Integer)
|
refresh_expires_at = Column(Integer)
|
||||||
user_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'))
|
user_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'))
|
||||||
service = None # for API-equivalence with APIToken
|
service = None # for API-equivalence with APIToken
|
||||||
|
Reference in New Issue
Block a user