Refresh user session when refreshing XSRF token. (#1804)

* Refresh user session when refreshing XSRF token.

* Refresh the user session on each page navigation, and fix a bug in XSRF refresh.
This commit is contained in:
Jason Robbins 2022-03-25 16:46:40 -07:00 коммит произвёл GitHub
Родитель c01c3dee7f
Коммит a9960a0de1
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
8 изменённых файлов: 50 добавлений и 28 удалений

Просмотреть файл

@ -17,10 +17,9 @@ import logging
from google.oauth2 import id_token from google.oauth2 import id_token
from google.auth.transport import requests from google.auth.transport import requests
from flask import session
from framework import basehandlers from framework import basehandlers
from framework import xsrf from framework import users
import settings import settings
@ -37,11 +36,7 @@ class LoginAPI(basehandlers.APIHandler):
idinfo = id_token.verify_oauth2_token( idinfo = id_token.verify_oauth2_token(
token, requests.Request(), token, requests.Request(),
settings.GOOGLE_SIGN_IN_CLIENT_ID) settings.GOOGLE_SIGN_IN_CLIENT_ID)
user_info = { users.add_signed_user_info_to_session(idinfo['email'])
'email': idinfo['email'],
}
signature = xsrf.generate_token(str(user_info))
session['signed_user_info'] = user_info, signature
message = "Done" message = "Done"
# print(idinfo['email'], file=sys.stderr) # print(idinfo['email'], file=sys.stderr)
except ValueError: except ValueError:

Просмотреть файл

@ -46,25 +46,18 @@ class LoginAPITest(testing_config.CustomTestCase):
"""We reject login requests that have an invalid credential_token.""" """We reject login requests that have an invalid credential_token."""
params = {'credential': 'fake bad token'} params = {'credential': 'fake bad token'}
with test_app.test_request_context(self.request_path, json=params): with test_app.test_request_context(self.request_path, json=params):
session['something else'] = 'some other aspect of the session' session.clear()
actual_response = self.handler.do_post() actual_response = self.handler.do_post()
self.assertEqual({'message': 'Invalid token'}, actual_response) self.assertEqual({'message': 'Invalid token'}, actual_response)
self.assertEqual(1, len(session)) self.assertNotIn('signed_user_info', session)
@mock.patch('google.oauth2.id_token.verify_oauth2_token') @mock.patch('google.oauth2.id_token.verify_oauth2_token')
def test_post__normal(self, mock_verify): def test_post__normal(self, mock_verify):
"""We log in the user if they provide a good credential_token.""" """We log in the user if they provide a good credential_token."""
mock_verify.return_value = {'email': 'user@example.com'} mock_verify.return_value = {'email': 'user@example.com'}
params = {'credential': 'fake bad token'} params = {'credential': 'fake good token'}
with test_app.test_request_context(self.request_path, json=params): with test_app.test_request_context(self.request_path, json=params):
session.clear() session.clear()
session['something else'] = 'some other aspect of the session'
actual_response = self.handler.do_post() actual_response = self.handler.do_post()
self.assertEqual({'message': 'Done'}, actual_response) self.assertEqual({'message': 'Done'}, actual_response)
self.assertEqual(2, len(session)) self.assertIn('signed_user_info', session)
user_info, signature = session['signed_user_info']
self.assertEqual({'email': 'user@example.com'}, user_info)
xsrf.validate_token(
signature,
str(user_info),
timeout=xsrf.REFRESH_TOKEN_TIMEOUT_SEC)

Просмотреть файл

@ -13,14 +13,11 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import logging import logging
from framework import basehandlers from framework import basehandlers
from framework import xsrf from framework import xsrf
from internals import models from framework import users
class TokenRefreshAPI(basehandlers.APIHandler): class TokenRefreshAPI(basehandlers.APIHandler):
@ -41,8 +38,9 @@ class TokenRefreshAPI(basehandlers.APIHandler):
# Note: we use only POST instead of GET to avoid attacks that use GETs. # Note: we use only POST instead of GET to avoid attacks that use GETs.
def do_post(self): def do_post(self):
"""Return a new XSRF token for the current user.""" """Refresh the session and return a new XSRF token for the current user."""
user = self.get_current_user() user = self.get_current_user()
users.refresh_user_session()
result = { result = {
'token': xsrf.generate_token(user.email()), 'token': xsrf.generate_token(user.email()),
'token_expires_sec': xsrf.token_expires_sec(), 'token_expires_sec': xsrf.token_expires_sec(),

Просмотреть файл

@ -15,6 +15,7 @@
import testing_config # Must be imported before the module under test. import testing_config # Must be imported before the module under test.
import flask import flask
from flask import session
from unittest import mock from unittest import mock
import werkzeug.exceptions # Flask HTTP stuff. import werkzeug.exceptions # Flask HTTP stuff.
@ -22,6 +23,7 @@ from api import token_refresh_api
from framework import xsrf from framework import xsrf
test_app = flask.Flask(__name__) test_app = flask.Flask(__name__)
test_app.secret_key = 'testing secret'
class TokenRefreshAPITest(testing_config.CustomTestCase): class TokenRefreshAPITest(testing_config.CustomTestCase):
@ -73,9 +75,12 @@ class TokenRefreshAPITest(testing_config.CustomTestCase):
def test_do_post__OK(self): def test_do_post__OK(self):
"""If the request is accepted, we return a new token.""" """If the request is accepted, we return a new token."""
testing_config.sign_in('user@example.com', 111)
params = {'token': 'checked in base class'} params = {'token': 'checked in base class'}
with test_app.test_request_context(self.request_path, json=params): with test_app.test_request_context(self.request_path, json=params):
session.clear()
testing_config.sign_in('user@example.com', 111)
actual = self.handler.do_post() actual = self.handler.do_post()
self.assertIn('token', actual)
self.assertIn('token_expires_sec', actual) self.assertIn('signed_user_info', session)
self.assertIn('token', actual)
self.assertIn('token_expires_sec', actual)

Просмотреть файл

@ -29,6 +29,7 @@ from framework import csp
from framework import permissions from framework import permissions
from framework import ramcache from framework import ramcache
from framework import secrets from framework import secrets
from framework import users
from framework import utils from framework import utils
from framework import xsrf from framework import xsrf
from internals import approval_defs from internals import approval_defs
@ -40,7 +41,6 @@ import django
from google.auth.transport import requests from google.auth.transport import requests
from flask import session from flask import session
import sys import sys
from framework import users
# Initialize django so that it'll function when run as a standalone script. # Initialize django so that it'll function when run as a standalone script.
# https://django.readthedocs.io/en/latest/releases/1.7.html#standalone-scripts # https://django.readthedocs.io/en/latest/releases/1.7.html#standalone-scripts
@ -328,6 +328,7 @@ class FlaskHandler(BaseHandler):
ramcache.check_for_distributed_invalidation() ramcache.check_for_distributed_invalidation()
handler_data = self.get_template_data(*args, **kwargs) handler_data = self.get_template_data(*args, **kwargs)
users.refresh_user_session()
if self.JSONIFY and type(handler_data) in (dict, list): if self.JSONIFY and type(handler_data) in (dict, list):
headers = self.get_headers() headers = self.get_headers()

Просмотреть файл

@ -228,3 +228,19 @@ def get_current_user():
def is_current_user_admin(): def is_current_user_admin():
return False return False
def add_signed_user_info_to_session(email):
"""Create and sign the user info in the Flask session."""
user_info = {
'email': email,
}
signature = xsrf.generate_token(str(user_info))
session['signed_user_info'] = user_info, signature
def refresh_user_session():
"""If the user is signed in, update the signed user info with a new date."""
user = get_current_user()
if user:
add_signed_user_info_to_session(user.email())

Просмотреть файл

@ -98,3 +98,17 @@ class UsersTest(testing_config.CustomTestCase):
"""We never consider a user an admin based on old GAE auth info.""" """We never consider a user an admin based on old GAE auth info."""
actual = users.is_current_user_admin() actual = users.is_current_user_admin()
self.assertFalse(actual) self.assertFalse(actual)
def test_add_signed_user_info_to_session(self):
"""We log in the user by adding a signed user_info to the session."""
with test_app.test_request_context('/any/path'):
session.clear()
session['something else'] = 'some other aspect of the session'
users.add_signed_user_info_to_session('user@example.com')
self.assertEqual(2, len(session))
user_info, signature = session['signed_user_info']
self.assertEqual({'email': 'user@example.com'}, user_info)
xsrf.validate_token(
signature,
str(user_info),
timeout=xsrf.REFRESH_TOKEN_TIMEOUT_SEC)

Просмотреть файл

@ -31,7 +31,7 @@ class ChromeStatusClient {
const refreshResponse = await this.doFetch( const refreshResponse = await this.doFetch(
'/currentuser/token', 'POST', null); '/currentuser/token', 'POST', null);
this.token = refreshResponse.token; this.token = refreshResponse.token;
this.tokenExpiresSec = refreshResponse.tokenExpiresSec; this.tokenExpiresSec = refreshResponse.token_expires_sec;
} }
} }