From 06f0d12f7f05ba30ce0d6be6047daeb5ce59fd0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20LE=20M=C3=89NER?= Date: Fri, 3 Jun 2022 11:03:13 +0200 Subject: [PATCH 1/5] Extract common code into common function --- oauthenticator/gitlab.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/oauthenticator/gitlab.py b/oauthenticator/gitlab.py index 5d0fbd09..4d00012b 100644 --- a/oauthenticator/gitlab.py +++ b/oauthenticator/gitlab.py @@ -128,7 +128,9 @@ async def authenticate(self, handler, data=None): grant_type="authorization_code", redirect_uri=self.get_callback_url(handler), ) + return await self._oauth_call(handler, params, data) + async def _oauth_call(self, handler, params, data=None): validate_server_cert = self.validate_server_cert url = url_concat("%s/oauth/token" % self.gitlab_url, params) From aa65f550cec62b85af8edf0c7d560060d054e631 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20LE=20M=C3=89NER?= Date: Fri, 3 Jun 2022 11:04:06 +0200 Subject: [PATCH 2/5] Rework common code so it can work with refresh --- oauthenticator/gitlab.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/oauthenticator/gitlab.py b/oauthenticator/gitlab.py index 4d00012b..1c2179c2 100644 --- a/oauthenticator/gitlab.py +++ b/oauthenticator/gitlab.py @@ -131,6 +131,9 @@ async def authenticate(self, handler, data=None): return await self._oauth_call(handler, params, data) async def _oauth_call(self, handler, params, data=None): + """ + Common logic shared by authenticate() and refresh_user() + """ validate_server_cert = self.validate_server_cert url = url_concat("%s/oauth/token" % self.gitlab_url, params) @@ -143,8 +146,8 @@ async def _oauth_call(self, handler, params, data=None): body='', # Body is required for a POST... ) - resp_json = await self.fetch(req, label="getting access token") - access_token = resp_json['access_token'] + oauth_resp = await self.fetch(req, label="getting access token") + access_token = oauth_resp['access_token'] # memoize gitlab version for class lifetime if self.gitlab_version is None: @@ -158,11 +161,10 @@ async def _oauth_call(self, handler, params, data=None): validate_cert=validate_server_cert, headers=_api_headers(access_token), ) - resp_json = await self.fetch(req, label="getting gitlab user") + gitlab_user = await self.fetch(req, label="getting gitlab user") - username = resp_json["username"] - user_id = resp_json["id"] - is_admin = resp_json.get("is_admin", False) + username = gitlab_user["username"] + user_id = gitlab_user["id"] # Check if user is a member of any allowed groups or projects. # These checks are performed here, as it requires `access_token`. @@ -191,10 +193,13 @@ async def _oauth_call(self, handler, params, data=None): ): return { 'name': username, - 'auth_state': {'access_token': access_token, 'gitlab_user': resp_json}, + 'auth_state': {**oauth_resp, **{'gitlab_user': gitlab_user}}, } else: - self.log.warning("%s not in group or project allowed list", username) + self.log.warning( + "%s not in group or project allowed list", + username, + ) return None async def _get_gitlab_version(self, access_token): From ccbf7ed515f4b9ca90f4473c1e9395b44ee3f904 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20LE=20M=C3=89NER?= Date: Fri, 3 Jun 2022 11:05:49 +0200 Subject: [PATCH 3/5] Implement the refresh_user --- oauthenticator/gitlab.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/oauthenticator/gitlab.py b/oauthenticator/gitlab.py index 1c2179c2..f42b9444 100644 --- a/oauthenticator/gitlab.py +++ b/oauthenticator/gitlab.py @@ -130,6 +130,38 @@ async def authenticate(self, handler, data=None): ) return await self._oauth_call(handler, params, data) + async def refresh_user(self, user, handler=None): + # Renew the Access Token with a valid Refresh Token + # + # See: https://github.com/gitlabhq/gitlabhq/blob/HEAD/doc/api/oauth2.md + auth_state = await user.get_auth_state() + if not auth_state: + self.log.info( + "No auth_state found for user %s refresh, full authentication needed", + user, + ) + return False + # In seconds, ex : 1607635748 + created_at = auth_state.get('created_at', 0) + # In seconds, ex : 7200 + expires_in = auth_state.get('expires_in', 0) + is_expired = created_at + expires_in - time.time() < 0 + if not is_expired: + # Access token still valid, function returns True + self.log.info( + "access_token still valid for user %s, refresh skipped", + user, + ) + return True + # GitLab specifies a POST request yet requires URL parameters + params = dict( + client_id=self.client_id, + client_secret=self.client_secret, + grant_type="refresh_token", + refresh_token=auth_state['refresh_token'], + ) + return await self._oauth_call(handler, params) + async def _oauth_call(self, handler, params, data=None): """ Common logic shared by authenticate() and refresh_user() From 54ae8e1e3900574b958e69b445551a63c6667895 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20LE=20M=C3=89NER?= Date: Fri, 3 Jun 2022 11:08:23 +0200 Subject: [PATCH 4/5] Add missing import --- oauthenticator/gitlab.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/oauthenticator/gitlab.py b/oauthenticator/gitlab.py index f42b9444..f79e4e0b 100644 --- a/oauthenticator/gitlab.py +++ b/oauthenticator/gitlab.py @@ -2,6 +2,7 @@ Custom Authenticator to use GitLab OAuth with JupyterHub """ import os +import time import warnings from urllib.parse import quote @@ -131,9 +132,11 @@ async def authenticate(self, handler, data=None): return await self._oauth_call(handler, params, data) async def refresh_user(self, user, handler=None): + # Renew the Access Token with a valid Refresh Token # # See: https://github.com/gitlabhq/gitlabhq/blob/HEAD/doc/api/oauth2.md + auth_state = await user.get_auth_state() if not auth_state: self.log.info( @@ -141,6 +144,7 @@ async def refresh_user(self, user, handler=None): user, ) return False + # In seconds, ex : 1607635748 created_at = auth_state.get('created_at', 0) # In seconds, ex : 7200 @@ -153,6 +157,7 @@ async def refresh_user(self, user, handler=None): user, ) return True + # GitLab specifies a POST request yet requires URL parameters params = dict( client_id=self.client_id, From 501264e804484ee350df1ebc5eaa28e2f24ba39c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20LE=20M=C3=89NER?= Date: Fri, 3 Jun 2022 11:25:32 +0200 Subject: [PATCH 5/5] Expand refresh_user() docstring --- oauthenticator/gitlab.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/oauthenticator/gitlab.py b/oauthenticator/gitlab.py index f79e4e0b..3f536e47 100644 --- a/oauthenticator/gitlab.py +++ b/oauthenticator/gitlab.py @@ -135,7 +135,13 @@ async def refresh_user(self, user, handler=None): # Renew the Access Token with a valid Refresh Token # - # See: https://github.com/gitlabhq/gitlabhq/blob/HEAD/doc/api/oauth2.md + # Without that custom configuration, the Gitlab access token gets + # outdated while the user can still connect to JupyterHub, that leads to + # forbidden Git interactions with Gitlab within Notebook. + # + # See: + # - https://github.com/gitlabhq/gitlabhq/blob/HEAD/doc/api/oauth2.md + # - https://github.com/jupyterhub/oauthenticator/pull/490 auth_state = await user.get_auth_state() if not auth_state: