admin: require email verification for admin login/setup

This commit is contained in:
Aaron 2019-02-21 19:39:37 -06:00
parent 64740b9f37
commit 3f7c90a381
No known key found for this signature in database
GPG Key ID: 3B5B7597106F0A0E
5 changed files with 63 additions and 40 deletions

View File

@ -23,6 +23,7 @@ const STATE = {
totpUri: '',
is2faAuthed: false,
backupCodeCount: 0,
isEmailVerified: false,
// local
loaded: false,
hasReadSetup: false,
@ -58,9 +59,24 @@ class MFAuth extends React.Component<{}, State> {
showQrCode,
isVerifying,
backupCodeCount,
isEmailVerified,
} = this.state;
const emailNotVerifiedWarning = loaded &&
!isEmailVerified && (
<Alert
type="error"
message={
<>
You must <b>verify your email</b> in order to act as admin. You should have
received an email with instructions when you signed up.
</>
}
/>
);
const lowBackupCodesWarning = loaded &&
has2fa &&
backupCodeCount < 5 && (
<Alert
type="warning"
@ -75,10 +91,12 @@ class MFAuth extends React.Component<{}, State> {
const wrap = (children: ReactNode) => (
<div className="MFAuth">
<>
{lowBackupCodesWarning}
{children}
</>
{emailNotVerifiedWarning || (
<>
{lowBackupCodesWarning}
{children}
</>
)}
</div>
);

View File

@ -41,13 +41,27 @@ from .example_emails import example_email_args
blueprint = Blueprint('admin', __name__, url_prefix='/api/v1/admin')
def make_2fa_state():
return {
"isLoginFresh": admin.is_auth_fresh(),
"has2fa": admin.has_2fa_setup(),
"is2faAuthed": admin.admin_is_2fa_authed(),
"backupCodeCount": admin.backup_code_count(),
"isEmailVerified": auth.is_email_verified(),
}
def make_login_state():
return {
"isLoggedIn": admin.admin_is_authed(),
"is2faAuthed": admin.admin_is_2fa_authed()
}
@blueprint.route("/checklogin", methods=["GET"])
@endpoint.api()
def loggedin():
return {
"isLoggedIn": admin.admin_is_authed(),
"is2faAuthed": admin.admin_is_2fa_authed(),
}
return make_login_state()
@blueprint.route("/login", methods=["POST"])
@ -58,10 +72,7 @@ def loggedin():
def login(username, password):
if auth.auth_user(username, password):
if admin.admin_is_authed():
return {
"isLoggedIn": admin.admin_is_authed(),
"is2faAuthed": admin.admin_is_2fa_authed()
}
return make_login_state()
return {"message": "Username or password incorrect."}, 401
@ -71,23 +82,11 @@ def login(username, password):
)
def refresh(password):
if auth.refresh_auth(password):
return {
"isLoggedIn": admin.admin_is_authed(),
"is2faAuthed": admin.admin_is_2fa_authed()
}
return make_login_state()
else:
return {"message": "Username or password incorrect."}, 401
def make_2fa_state():
return {
"isLoginFresh": admin.is_auth_fresh(),
"has2fa": admin.has_2fa_setup(),
"is2faAuthed": admin.admin_is_2fa_authed(),
"backupCodeCount": admin.backup_code_count(),
}
@blueprint.route("/2fa", methods=["GET"])
@endpoint.api()
def get_2fa():
@ -99,10 +98,7 @@ def get_2fa():
@blueprint.route("/2fa/init", methods=["GET"])
@endpoint.api()
def get_2fa_init():
if not admin.admin_is_authed():
return {"message": "Must be authenticated"}, 403
if not admin.is_auth_fresh():
return {"message": "Login stale"}, 403
admin.throw_on_2fa_not_allowed()
return admin.make_2fa_setup()
@ -113,10 +109,7 @@ def get_2fa_init():
parameter('verifyCode', type=str, required=True),
)
def post_2fa_enable(backup_codes, totp_secret, verify_code):
if not admin.admin_is_authed():
return {"message": "Must be authenticated"}, 403
if not admin.is_auth_fresh():
return {"message": "Login stale"}, 403
admin.throw_on_2fa_not_allowed()
admin.check_and_set_2fa_setup(backup_codes, totp_secret, verify_code)
db.session.commit()
return make_2fa_state()
@ -127,10 +120,7 @@ def post_2fa_enable(backup_codes, totp_secret, verify_code):
parameter('verifyCode', type=str, required=True),
)
def post_2fa_verify(verify_code):
if not admin.admin_is_authed():
return {"message": "Must be authenticated"}, 403
if not admin.is_auth_fresh():
return {"message": "Login stale"}, 403
admin.throw_on_2fa_not_allowed()
admin.admin_auth_2fa(verify_code)
db.session.commit()
return make_2fa_state()

View File

@ -1,7 +1,7 @@
from functools import wraps
from datetime import datetime
from .auth import auth_user, get_authed_user, throw_on_banned, is_auth_fresh, AuthException, logout_current_user
from .auth import auth_user, get_authed_user, throw_on_banned, is_auth_fresh, AuthException, logout_current_user, is_email_verified
from .totp_2fa import gen_backup_codes, gen_otp_secret, gen_uri, verify_totp, verify_and_update_backup_codes
from hashlib import sha256
@ -82,6 +82,15 @@ def make_2fa_setup():
}
def throw_on_2fa_not_allowed():
if not admin_is_authed():
raise AuthException("Must be authenticated")
if not is_auth_fresh():
raise AuthException("Login stale")
if not is_email_verified():
raise AuthException("Email must be verified")
def check_and_set_2fa_setup(codes: tuple, secret: str, verify: str):
if '2fa_setup_hash' not in session:
raise AuthException("Could not find a setup hash to check")

View File

@ -35,6 +35,11 @@ def is_auth_fresh(minutes: int=20):
return now - last < timedelta(minutes=minutes)
def is_email_verified():
user = get_authed_user()
return user.email_verification.has_verified
def auth_user(email, password):
existing_user = User.get_by_email(email)
if not existing_user:

View File

@ -21,7 +21,8 @@ json_2fa = {
"isLoginFresh": True,
"has2fa": False,
"is2faAuthed": False,
"backupCodeCount": 0
"backupCodeCount": 0,
"isEmailVerified": True,
}
@ -168,7 +169,7 @@ class TestAdminAPI(BaseProposalCreatorConfig):
# 14. 2fa verify (fail; logged out)
r = self.p("/api/v1/admin/2fa/verify", {'verifyCode': totp_2fa.current_totp(secret)})
self.assert403(r)
self.assert_autherror(r, 'Must be auth')
# 15. login
r = send_login()