Skip to content

Commit c8c8a2b

Browse files
authored
Merge branch 'bugy:master' into master
2 parents d4c30ac + d65eb5d commit c8c8a2b

20 files changed

+996
-133
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ or [how to configure the server](https://github.com/bugy/script-server/wiki/Serv
3232

3333
### Server-side
3434

35-
Python 3.6 or higher with the following modules:
35+
Python 3.7 or higher with the following modules:
3636

3737
* Tornado 5 / 6
3838

src/auth/auth_abstract_oauth.py

Lines changed: 78 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import abc
22
import asyncio
3+
import datetime
34
import json
45
import logging
56
import os
@@ -15,11 +16,12 @@
1516

1617
from auth import auth_base
1718
from auth.auth_base import AuthFailureError, AuthBadRequestException, AuthRejectedError
19+
from auth.oauth_token_manager import OAuthTokenManager
20+
from auth.oauth_token_response import OAuthTokenResponse
1821
from model import model_helper
1922
from model.model_helper import read_bool_from_config, read_int_from_config
2023
from model.server_conf import InvalidServerConfigException
2124
from utils import file_utils
22-
from utils.tornado_utils import get_secure_cookie
2325

2426
LOGGER = logging.getLogger('script_server.AbstractOauthAuthenticator')
2527

@@ -90,6 +92,12 @@ def __init__(self, oauth_authorize_url, oauth_token_url, oauth_scope, params_dic
9092

9193
self._schedule_dump_task()
9294

95+
self._token_manager = OAuthTokenManager(
96+
enabled=bool(self.auth_info_ttl),
97+
fetch_token_callback=self._fetch_token_by_refresh)
98+
99+
self.ioloop = tornado.ioloop.IOLoop.current()
100+
93101
@staticmethod
94102
def _validate_dump_file(dump_file):
95103
if os.path.isdir(dump_file):
@@ -105,8 +113,8 @@ async def authenticate(self, request_handler):
105113
LOGGER.error('Code is not specified')
106114
raise AuthBadRequestException('Missing authorization information. Please contact your administrator')
107115

108-
(access_token, refresh_token) = await self.fetch_access_token(code, request_handler)
109-
user_info = await self.fetch_user_info(access_token)
116+
token_response = await self.fetch_access_token_by_code(code, request_handler)
117+
user_info = await self.fetch_user_info(token_response.access_token)
110118

111119
username = user_info.username
112120
if not username:
@@ -124,12 +132,13 @@ async def authenticate(self, request_handler):
124132
self._users[username] = user_state
125133

126134
if self.group_support:
127-
await self.load_groups(access_token, username, user_info, user_state)
135+
await self.load_groups(token_response.access_token, username, user_info, user_state)
128136

129137
now = time.time()
130138

139+
self._token_manager.update_tokens(token_response, username, request_handler)
140+
131141
if self.auth_info_ttl:
132-
request_handler.set_secure_cookie('token', access_token)
133142
user_state.last_auth_update = now
134143

135144
user_state.last_visit = now
@@ -144,23 +153,28 @@ async def load_groups(self, access_token, username, user_info, user_state):
144153
user_state.groups = user_groups
145154
LOGGER.info('Loaded groups for ' + username + ': ' + str(user_state.groups))
146155

147-
def validate_user(self, user, request_handler):
156+
async def validate_user(self, user, request_handler):
148157
if not user:
149158
LOGGER.warning('Username is not available')
150159
return False
151160

152161
now = time.time()
153162

154163
user_state = self._users.get(user)
164+
validate_expiration = True
155165
if not user_state:
156166
# if nothing is enabled, it's ok not to have user state (e.g. after server restart)
157167
if self.session_expire <= 0 and not self.auth_info_ttl and not self.group_support:
158168
return True
169+
elif self._token_manager.can_restore_state(request_handler):
170+
validate_expiration = False
171+
user_state = _UserState(user)
172+
self._users[user] = user_state
159173
else:
160174
LOGGER.info('User %s state is missing', user)
161175
return False
162176

163-
if self.session_expire > 0:
177+
if (self.session_expire > 0) and validate_expiration:
164178
last_visit = user_state.last_visit
165179
if (last_visit is None) or ((last_visit + self.session_expire) < now):
166180
LOGGER.info('User %s state is expired', user)
@@ -169,9 +183,10 @@ def validate_user(self, user, request_handler):
169183
user_state.last_visit = now
170184

171185
if self.auth_info_ttl:
172-
access_token = get_secure_cookie(request_handler, 'token')
186+
access_token = await self._token_manager.synchronize_user_tokens(user, request_handler)
173187
if access_token is None:
174188
LOGGER.info('User %s token is not available', user)
189+
self._remove_user(user)
175190
return False
176191

177192
self.update_user_auth(user, user_state, access_token)
@@ -186,57 +201,40 @@ def get_groups(self, user, known_groups=None):
186201
return user_state.groups
187202

188203
def logout(self, user, request_handler):
189-
request_handler.clear_cookie('token')
204+
self._token_manager.logout(user, request_handler)
190205
self._remove_user(user)
191206

192207
self._dump_state()
193208

194209
def _remove_user(self, user):
195210
if user in self._users:
196211
del self._users[user]
212+
self._token_manager.remove_user(user)
197213

198-
async def fetch_access_token(self, code, request_handler):
199-
body = urllib_parse.urlencode({
214+
async def fetch_access_token_by_code(self, code, request_handler):
215+
return await self._fetch_token({
200216
'redirect_uri': get_path_for_redirect(request_handler),
201217
'code': code,
202218
'client_id': self.client_id,
203219
'client_secret': self.secret,
204220
'grant_type': 'authorization_code',
205221
})
206222

207-
response = await self.http_client.fetch(
208-
self.oauth_token_url,
209-
method='POST',
210-
headers={'Content-Type': 'application/x-www-form-urlencoded'},
211-
body=body,
212-
raise_error=False)
213-
214-
response_values = {}
215-
if response.body:
216-
response_values = escape.json_decode(response.body)
217-
218-
if response.error:
219-
if response_values.get('error_description'):
220-
error_text = response_values.get('error_description')
221-
elif response_values.get('error'):
222-
error_text = response_values.get('error')
223-
else:
224-
error_text = str(response.error)
225-
226-
error_message = 'Failed to load access_token: ' + error_text
227-
LOGGER.error(error_message)
228-
raise AuthFailureError(error_message)
229-
230-
response_values = escape.json_decode(response.body)
231-
access_token = response_values.get('access_token')
232-
refresh_token = response_values.get('refresh_token')
233-
234-
if not access_token:
235-
message = 'No access token in response: ' + str(response.body)
236-
LOGGER.error(message)
237-
raise AuthFailureError(message)
238-
239-
return access_token, refresh_token
223+
async def _fetch_token_by_refresh(self, refresh_token, username):
224+
if username not in self._users:
225+
return None
226+
227+
try:
228+
return await self._fetch_token({
229+
'refresh_token': refresh_token,
230+
'client_id': self.client_id,
231+
'client_secret': self.secret,
232+
'grant_type': 'refresh_token',
233+
})
234+
except AuthFailureError:
235+
LOGGER.info(f'Failed to refresh token for user {username}. Logging out')
236+
self._remove_user(username)
237+
return None
240238

241239
def update_user_auth(self, username, user_state, access_token):
242240
now = time.time()
@@ -246,7 +244,7 @@ def update_user_auth(self, username, user_state, access_token):
246244
if not ttl_expired:
247245
return
248246

249-
tornado.ioloop.IOLoop.current().spawn_callback(
247+
self.ioloop.spawn_callback(
250248
self._do_update_user_auth_async,
251249
username,
252250
user_state,
@@ -342,6 +340,41 @@ def _cleanup(self):
342340
if self.timer:
343341
self.timer.cancel()
344342

343+
async def _fetch_token(self, body):
344+
encoded_body = urllib_parse.urlencode(body)
345+
346+
response = await self.http_client.fetch(
347+
self.oauth_token_url,
348+
method='POST',
349+
headers={'Content-Type': 'application/x-www-form-urlencoded'},
350+
body=encoded_body,
351+
raise_error=False)
352+
353+
response_values = {}
354+
if response.body:
355+
response_values = escape.json_decode(response.body)
356+
357+
if response.error:
358+
if response_values.get('error_description'):
359+
error_text = response_values.get('error_description')
360+
elif response_values.get('error'):
361+
error_text = response_values.get('error')
362+
else:
363+
error_text = str(response.error)
364+
365+
error_message = 'Failed to refresh access_token: ' + error_text
366+
LOGGER.error(error_message)
367+
raise AuthFailureError(error_message)
368+
369+
token_response = OAuthTokenResponse.create(response_values, datetime.datetime.now())
370+
371+
if not token_response.access_token:
372+
message = 'No access token in response: ' + str(response.body)
373+
LOGGER.error(message)
374+
raise AuthFailureError(message)
375+
376+
return token_response
377+
345378

346379
def get_path_for_redirect(request_handler):
347380
referer = request_handler.request.headers.get('Referer')

src/auth/auth_base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def get_client_visible_config(self):
1717
def get_groups(self, user, known_groups=None):
1818
return []
1919

20-
def validate_user(self, user, request_handler):
20+
async def validate_user(self, user, request_handler):
2121
return True
2222

2323
def perform_basic_auth(self, user, password):

src/auth/auth_gitlab.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def __init__(self, params_dict):
2929
async def fetch_user_info(self, access_token) -> _OauthUserInfo:
3030
user = await self.oauth2_request(
3131
_OAUTH_GITLAB_USERINFO % self.gitlab_host,
32-
access_token)
32+
access_token=access_token)
3333
if user is None:
3434
return None
3535

src/auth/identification.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@
22
import logging
33
import uuid
44

5-
import tornado.websocket
6-
75
from model.trusted_ips import TrustedIpValidator
86
from utils import tornado_utils, date_utils, audit_utils
97
from utils.date_utils import days_to_ms
8+
from utils.tornado_utils import can_write_secure_cookie
109

1110
LOGGER = logging.getLogger('identification')
1211

@@ -120,4 +119,4 @@ def _write_client_token(self, client_id, request_handler):
120119
request_handler.set_secure_cookie(self.COOKIE_KEY, new_token, expires_days=self.EXPIRES_DAYS)
121120

122121
def _can_write(self, request_handler):
123-
return not isinstance(request_handler, tornado.websocket.WebSocketHandler)
122+
return can_write_secure_cookie(request_handler)

0 commit comments

Comments
 (0)