check users' session id.

when a user enters their 2FA code, the API will store a random UUID
against them in the database - this code is then stored on the cookie
on the front end.

At the beginning of each authenticated request, we do the following
steps:
  * Retrieve the user's cookie, and get the user_id from it
  * Request that user's details from the database
  * populate current_user with the DB model
  * run the login_required decorator, which calls
    current_user.is_authenticated

is_authenticated now also checks that the database model matches the
cookie for session_id. The potential states and meanings are as follows:

 database | cookie | meaning
----------+--------+---------
 None     | None   | New user, or system just been deployed.
          |        | Redirect to start page.
----------+--------+---------
 'abc'    | None   | New browser (or cleared cookies). Redirect to
          |        | start page.
----------+--------+---------
 None     | 'abc'  | Invalid state (cookie is set from user obj, so
          |        | would only happen if DB is cleared)
----------+--------+---------
 'abc'    | 'abc'  | Same browser. Business as usual
----------+--------+---------
 'abc'    | 'def'  | Different browser in cookie - db has been changed
          |        | since then. Redirect to start
This commit is contained in:
Leo Hemsted
2017-02-17 14:06:09 +00:00
parent aad891d4ce
commit f14a836baa
8 changed files with 119 additions and 27 deletions

View File

@@ -1,4 +1,5 @@
from flask_login import (UserMixin, login_fresh)
from flask_login import UserMixin, AnonymousUserMixin, login_fresh
from flask import session
class User(UserMixin):
@@ -13,20 +14,25 @@ class User(UserMixin):
self._state = fields.get('state')
self.max_failed_login_count = max_failed_login_count
self.platform_admin = fields.get('platform_admin')
self.current_session_id = fields.get('current_session_id')
def get_id(self):
return self.id
def logged_in_elsewhere(self):
return session.get('current_session_id') != self.current_session_id
@property
def is_active(self):
return self.state == 'active'
@property
def is_authenticated(self):
# To handle remember me token renewal
if not login_fresh():
return False
return super(User, self).is_authenticated
return (
login_fresh() and
not self.logged_in_elsewhere() and
super(User, self).is_authenticated
)
@property
def id(self):
@@ -114,15 +120,18 @@ class User(UserMixin):
return self.failed_login_count >= self.max_failed_login_count
def serialize(self):
dct = {"id": self.id,
"name": self.name,
"email_address": self.email_address,
"mobile_number": self.mobile_number,
"password_changed_at": self.password_changed_at,
"state": self.state,
"failed_login_count": self.failed_login_count,
"permissions": [x for x in self._permissions]}
if getattr(self, '_password', None):
dct = {
"id": self.id,
"name": self.name,
"email_address": self.email_address,
"mobile_number": self.mobile_number,
"password_changed_at": self.password_changed_at,
"state": self.state,
"failed_login_count": self.failed_login_count,
"permissions": [x for x in self._permissions],
"current_session_id": self.current_session_id
}
if hasattr(self, '_password'):
dct['password'] = self._password
return dct
@@ -174,3 +183,9 @@ class InvitedUser(object):
else:
data['permissions'] = self.permissions
return data
class AnonymousUser(AnonymousUserMixin):
# set the anonymous user so that if a new browser hits us we don't error http://stackoverflow.com/a/19275188
def logged_in_elsewhere(self):
return False

View File

@@ -1,3 +1,6 @@
import uuid
from flask import session
from notifications_python_client.errors import HTTPError
from app.notify_client import NotifyAdminAPIClient