Add the concept of require_fresh_login to both the backend and frontend. Sensitive methods will now be marked with the annotation, which requires that the user has performed a login within 10 minutes or they are asked to do so in the UI before running the operation again.
This commit is contained in:
parent
1e7e012b92
commit
e783df31e0
9 changed files with 174 additions and 61 deletions
|
@ -1,7 +1,8 @@
|
|||
import logging
|
||||
import json
|
||||
import datetime
|
||||
|
||||
from flask import Blueprint, request, make_response, jsonify
|
||||
from flask import Blueprint, request, make_response, jsonify, session
|
||||
from flask.ext.restful import Resource, abort, Api, reqparse
|
||||
from flask.ext.restful.utils.cors import crossdomain
|
||||
from werkzeug.exceptions import HTTPException
|
||||
|
@ -66,6 +67,11 @@ class Unauthorized(ApiException):
|
|||
ApiException.__init__(self, 'insufficient_scope', 403, 'Unauthorized', payload)
|
||||
|
||||
|
||||
class FreshLoginRequired(ApiException):
|
||||
def __init__(self, payload=None):
|
||||
ApiException.__init__(self, 'fresh_login_required', 401, "Requires fresh login", payload)
|
||||
|
||||
|
||||
class ExceedsLicenseException(ApiException):
|
||||
def __init__(self, payload=None):
|
||||
ApiException.__init__(self, None, 402, 'Payment Required', payload)
|
||||
|
@ -264,6 +270,26 @@ def require_user_permission(permission_class, scope=None):
|
|||
|
||||
require_user_read = require_user_permission(UserReadPermission, scopes.READ_USER)
|
||||
require_user_admin = require_user_permission(UserAdminPermission, None)
|
||||
require_fresh_user_admin = require_user_permission(UserAdminPermission, None)
|
||||
|
||||
def require_fresh_login(func):
|
||||
@add_method_metadata('requires_fresh_login', True)
|
||||
@wraps(func)
|
||||
def wrapped(*args, **kwargs):
|
||||
user = get_authenticated_user()
|
||||
if not user:
|
||||
raise Unauthorized()
|
||||
|
||||
logger.debug('Checking fresh login for user %s', user.username)
|
||||
|
||||
last_login = session.get('login_time', datetime.datetime.now() - datetime.timedelta(minutes=60))
|
||||
valid_span = datetime.datetime.now() - datetime.timedelta(minutes=10)
|
||||
|
||||
if last_login >= valid_span:
|
||||
return func(*args, **kwargs)
|
||||
|
||||
raise FreshLoginRequired()
|
||||
return wrapped
|
||||
|
||||
|
||||
def require_scope(scope_object):
|
||||
|
|
|
@ -119,6 +119,11 @@ def swagger_route_data(include_internal=False, compact=False):
|
|||
if internal is not None:
|
||||
new_operation['internal'] = True
|
||||
|
||||
if include_internal:
|
||||
requires_fresh_login = method_metadata(method, 'requires_fresh_login')
|
||||
if requires_fresh_login is not None:
|
||||
new_operation['requires_fresh_login'] = True
|
||||
|
||||
if not internal or (internal and include_internal):
|
||||
operations.append(new_operation)
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ from app import app, billing as stripe, authentication
|
|||
from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error,
|
||||
log_action, internal_only, NotFound, require_user_admin, parse_args,
|
||||
query_param, InvalidToken, require_scope, format_date, hide_if, show_if,
|
||||
license_error)
|
||||
license_error, require_fresh_login)
|
||||
from endpoints.api.subscribe import subscribe
|
||||
from endpoints.common import common_login
|
||||
from data import model
|
||||
|
@ -117,10 +117,6 @@ class User(ApiResource):
|
|||
'type': 'object',
|
||||
'description': 'Fields which can be updated in a user.',
|
||||
'properties': {
|
||||
'current_password': {
|
||||
'type': 'string',
|
||||
'description': 'The user\'s current password',
|
||||
},
|
||||
'password': {
|
||||
'type': 'string',
|
||||
'description': 'The user\'s password',
|
||||
|
@ -148,6 +144,7 @@ class User(ApiResource):
|
|||
return user_view(user)
|
||||
|
||||
@require_user_admin
|
||||
@require_fresh_login
|
||||
@nickname('changeUserDetails')
|
||||
@internal_only
|
||||
@validate_json_request('UpdateUser')
|
||||
|
@ -156,22 +153,8 @@ class User(ApiResource):
|
|||
user = get_authenticated_user()
|
||||
user_data = request.get_json()
|
||||
|
||||
def verify_current_password(user, user_data):
|
||||
current_password = user_data.get('current_password', '')
|
||||
|
||||
verified = False
|
||||
try:
|
||||
verified = model.verify_user(user.username, current_password)
|
||||
except:
|
||||
pass
|
||||
|
||||
if not verified:
|
||||
raise request_error(message='Current password does not match')
|
||||
|
||||
try:
|
||||
if 'password' in user_data:
|
||||
verify_current_password(user, user_data)
|
||||
|
||||
logger.debug('Changing password for user: %s', user.username)
|
||||
log_action('account_change_password', user.username)
|
||||
model.change_password(user, user_data['password'])
|
||||
|
@ -181,8 +164,6 @@ class User(ApiResource):
|
|||
model.change_invoice_email(user, user_data['invoice_email'])
|
||||
|
||||
if 'email' in user_data and user_data['email'] != user.email:
|
||||
verify_current_password(user, user_data)
|
||||
|
||||
new_email = user_data['email']
|
||||
if model.find_user_by_email(new_email):
|
||||
# Email already used.
|
||||
|
@ -377,6 +358,37 @@ class Signin(ApiResource):
|
|||
return conduct_signin(username, password)
|
||||
|
||||
|
||||
@resource('/v1/signin/verify')
|
||||
@internal_only
|
||||
class VerifyUser(ApiResource):
|
||||
""" Operations for verifying the existing user. """
|
||||
schemas = {
|
||||
'VerifyUser': {
|
||||
'id': 'VerifyUser',
|
||||
'type': 'object',
|
||||
'description': 'Information required to verify the signed in user.',
|
||||
'required': [
|
||||
'password',
|
||||
],
|
||||
'properties': {
|
||||
'password': {
|
||||
'type': 'string',
|
||||
'description': 'The user\'s password',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@require_user_admin
|
||||
@nickname('verifyUser')
|
||||
@validate_json_request('VerifyUser')
|
||||
def post(self):
|
||||
""" Verifies the signed in the user with the specified credentials. """
|
||||
signin_data = request.get_json()
|
||||
password = signin_data['password']
|
||||
return conduct_signin(get_authenticated_user().username, password)
|
||||
|
||||
|
||||
@resource('/v1/signout')
|
||||
@internal_only
|
||||
class Signout(ApiResource):
|
||||
|
|
Reference in a new issue