admin: require email verification for admin login/setup
This commit is contained in:
parent
64740b9f37
commit
3f7c90a381
|
@ -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>
|
||||
);
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue