add email code verification

by hitting POST /<user_id>/email-code, we create an email two factor
code to send to the user. That email contains a link with a token that
will sign the user in when opened.

Also some other things:

"email verification" (aka when you first create an account) doesn't
hit the API anymore

refactor 2fa code verification and sending to use jsonschema, and share code between sms and email

Die marshmallow die!
This commit is contained in:
Leo Hemsted
2017-11-03 09:51:50 +00:00
parent 8b2c242355
commit b2756ac99d
6 changed files with 109 additions and 78 deletions

View File

@@ -47,12 +47,7 @@ def get_user_code(user, code, code_type):
codes = VerifyCode.query.filter_by(
user=user, code_type=code_type).order_by(
VerifyCode.created_at.desc())
retval = None
for x in codes:
if x.check_code(code):
retval = x
break
return retval
return next((x for x in codes if x.check_code(code)), None)
def delete_codes_older_created_more_than_a_day_ago():

View File

@@ -366,14 +366,6 @@ class JobSchema(BaseSchema):
strict = True
class RequestVerifyCodeSchema(ma.Schema):
class Meta:
strict = True
to = fields.Str(required=False)
class NotificationSchema(ma.Schema):
class Meta:
@@ -653,7 +645,6 @@ api_key_schema = ApiKeySchema()
api_key_schema_load_json = ApiKeySchema(load_json=True)
job_schema = JobSchema()
job_schema_load_json = JobSchema(load_json=True)
request_verify_code_schema = RequestVerifyCodeSchema()
sms_admin_notification_schema = SmsAdminNotificationSchema()
sms_template_notification_schema = SmsTemplateNotificationSchema()
job_sms_template_notification_schema = JobSmsTemplateNotificationSchema()

View File

@@ -30,7 +30,6 @@ from app.notifications.process_notifications import (
from app.schemas import (
email_data_request_schema,
user_schema,
request_verify_code_schema,
permission_schema,
user_schema_load_json,
user_update_schema_load_json,
@@ -41,6 +40,12 @@ from app.errors import (
InvalidRequest
)
from app.utils import url_with_token
from app.user.users_schema import (
post_verify_code_schema,
post_send_user_sms_code_schema,
post_send_user_email_code_schema,
)
from app.schema_validation import validate
user_blueprint = Blueprint('user', __name__)
register_errors(user_blueprint)
@@ -115,24 +120,12 @@ def verify_user_password(user_id):
@user_blueprint.route('/<uuid:user_id>/verify/code', methods=['POST'])
def verify_user_code(user_id):
data = request.get_json()
validate(data, post_verify_code_schema)
user_to_verify = get_user_by_id(user_id=user_id)
req_json = request.get_json()
verify_code = None
code_type = None
errors = {}
try:
verify_code = req_json['code']
except KeyError:
errors.update({'code': ['Required field missing data']})
try:
code_type = req_json['code_type']
except KeyError:
errors.update({'code_type': ['Required field missing data']})
if errors:
raise InvalidRequest(errors, status_code=400)
code = get_user_code(user_to_verify, verify_code, code_type)
code = get_user_code(user_to_verify, data['code'], data['code_type'])
if user_to_verify.failed_login_count >= current_app.config.get('MAX_VERIFY_CODE_COUNT'):
raise InvalidRequest("Code not found", status_code=404)
if not code:
@@ -142,11 +135,10 @@ def verify_user_code(user_id):
increment_failed_login_count(user_to_verify)
raise InvalidRequest("Code has expired", status_code=400)
if code_type == 'sms':
user_to_verify.current_session_id = str(uuid.uuid4())
user_to_verify.logged_in_at = datetime.utcnow()
user_to_verify.failed_login_count = 0
save_model_user(user_to_verify)
user_to_verify.current_session_id = str(uuid.uuid4())
user_to_verify.logged_in_at = datetime.utcnow()
user_to_verify.failed_login_count = 0
save_model_user(user_to_verify)
use_user_code(code.id)
return jsonify({}), 204
@@ -154,29 +146,59 @@ def verify_user_code(user_id):
@user_blueprint.route('/<uuid:user_id>/sms-code', methods=['POST'])
def send_user_sms_code(user_id):
user_to_send_to = get_user_by_id(user_id=user_id)
verify_code, errors = request_verify_code_schema.load(request.get_json())
if count_user_verify_codes(user_to_send_to) >= current_app.config.get('MAX_VERIFY_CODE_COUNT'):
# Prevent more than `MAX_VERIFY_CODE_COUNT` active verify codes at a time
current_app.logger.warn('Max verify code has exceeded for user {}'.format(user_to_send_to.id))
data = request.get_json()
user_to_send_to = validate_2fa_call(user_id, data, post_send_user_sms_code_schema)
if not user_to_send_to:
return jsonify({}), 204
secret_code = create_secret_code()
create_user_code(user_to_send_to, secret_code, SMS_TYPE)
mobile = user_to_send_to.mobile_number if verify_code.get('to', None) is None else verify_code.get('to')
sms_code_template_id = current_app.config['SMS_CODE_TEMPLATE_ID']
sms_code_template = dao_get_template_by_id(sms_code_template_id)
service = Service.query.get(current_app.config['NOTIFY_SERVICE_ID'])
mobile = data.get('to') or user_to_send_to.mobile_number
template = dao_get_template_by_id(current_app.config['SMS_CODE_TEMPLATE_ID'])
personalisation = {'verify_code': secret_code},
create_2fa_code(template, mobile, personalisation)
return jsonify({}), 204
@user_blueprint.route('/<uuid:user_id>/email-code', methods=['POST'])
def send_user_email_code(user_id):
user_to_send_to = validate_2fa_call(user_id, request.get_json(), post_send_user_email_code_schema)
if not user_to_send_to:
return jsonify({}), 204
create_user_code(user_to_send_to, uuid.uuid4(), EMAIL_TYPE)
template = dao_get_template_by_id(current_app.config['EMAIL_CODE_TEMPLATE_ID'])
personalisation = {'url': _create_2fa_url(user_to_send_to)},
create_2fa_code(template, user_to_send_to.email_address, personalisation)
return '{}', 204
def validate_2fa_call(user_id, data, schema):
validate(data, schema)
user_to_send_to = get_user_by_id(user_id=user_id)
if count_user_verify_codes(user_to_send_to) >= current_app.config.get('MAX_VERIFY_CODE_COUNT'):
# Prevent more than `MAX_VERIFY_CODE_COUNT` active verify codes at a time
current_app.logger.warn('Max verify code has exceeded for user {}'.format(user_to_send_to.id))
return
return user_to_send_to
def create_2fa_code(template, recipient, personalisation):
saved_notification = persist_notification(
template_id=sms_code_template_id,
template_version=sms_code_template.version,
recipient=mobile,
service=service,
personalisation={'verify_code': secret_code},
notification_type=SMS_TYPE,
template_id=template.id,
template_version=template.version,
recipient=recipient,
service=template.service,
personalisation=personalisation,
notification_type=template.template_type,
api_key_id=None,
key_type=KEY_TYPE_NORMAL
)
@@ -185,8 +207,6 @@ def send_user_sms_code(user_id):
# admin even if we're doing user research using this service:
send_notification_to_queue(saved_notification, False, queue=QueueNames.NOTIFY)
return jsonify({}), 204
@user_blueprint.route('/<uuid:user_id>/change-email-verification', methods=['POST'])
def send_user_confirm_new_email(user_id):
@@ -219,6 +239,7 @@ def send_user_confirm_new_email(user_id):
@user_blueprint.route('/<uuid:user_id>/email-verification', methods=['POST'])
def send_user_email_verification(user_id):
# when registering, we verify all users' email addresses using this function
user_to_send_to = get_user_by_id(user_id=user_id)
secret_code = create_secret_code()
create_user_code(user_to_send_to, secret_code, 'email')
@@ -361,3 +382,12 @@ def _create_confirmation_url(user, email_address):
data = json.dumps({'user_id': str(user.id), 'email': email_address})
url = '/user-profile/email/confirm/'
return url_with_token(data, url, current_app.config)
def _create_2fa_url(user, next_redir=None):
data = json.dumps({'user_id': str(user.id), 'email': user.email_address})
url = '/email-auth/'
ret = url_with_token(data, url, current_app.config)
if next_redir:
ret += '?next={}'.format(next_redir)
return ret

35
app/user/users_schema.py Normal file
View File

@@ -0,0 +1,35 @@
post_verify_code_schema = {
'$schema': 'http://json-schema.org/draft-04/schema#',
'description': 'POST schema for verifying a 2fa code',
'type': 'object',
'properties': {
'code': {'type': 'string'},
'code_type': {'type': 'string'},
},
'required': ['code', 'code_type']
}
post_send_user_email_code_schema = {
'$schema': 'http://json-schema.org/draft-04/schema#',
'description': 'POST schema for generating a 2fa email',
'type': 'object',
'properties': {
# doesn't need 'to' as we'll just grab user.email_address
'next': {'type': ['string', 'null']},
},
'required': [],
'additionalProperties': []
}
post_send_user_sms_code_schema = {
'$schema': 'http://json-schema.org/draft-04/schema#',
'description': 'POST schema for generating a 2fa email',
'type': 'object',
'properties': {
'to': {'type': ['string', 'null']},
},
'required': [],
'additionalProperties': []
}