Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions source/app/blueprints/rest/v2/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

import jwt
import pyotp

from flask import Blueprint
from flask import session
from flask import redirect
Expand All @@ -26,6 +28,7 @@
from oic.oauth2.exception import GrantError

from app import app
from app import bc
from app.db import db
from app import oidc_client
from app.blueprints.iris_user import iris_current_user
Expand Down Expand Up @@ -86,6 +89,134 @@ def login():
return response_api_success(data=user_data)


@auth_blueprint.post('/mfa-setup')
def mfa_setup():
"""
Persist user's MFA secret after validating:
- refresh_token is valid (used to identify user_id)
- provided TOTP token matches provided secret
- provided password matches user (LDAP or local)
"""
data = request.get_json(silent=True) or {}

refresh_token = data.get('refresh_token')
token = data.get('token')
mfa_secret = data.get('mfa_secret')
user_password = data.get('user_password') or data.get('password')

if not refresh_token or not token or not mfa_secret or not user_password:
return response_api_error('Missing required fields: refresh_token, token, mfa_secret, password')

try:
payload = jwt.decode(refresh_token, app.config.get('SECRET_KEY'), algorithms=['HS256'])

if payload.get('type') != 'refresh':
return response_api_error('Invalid token type')

user_id = payload.get('user_id')
user = users_get_active(user_id)

totp = pyotp.TOTP(mfa_secret)
if not totp.verify(str(token)):
track_activity(
f"Failed MFA setup for user {user.user}. Invalid token.",
ctx_less=True,
display_in_ui=False,
)
return response_api_error('Invalid token')

has_valid_password = False

if is_authentication_ldap() is True:
if validate_ldap_login(
user.user,
user_password,
local_fallback=app.config.get("AUTHENTICATION_LOCAL_FALLBACK"),
):
has_valid_password = True
else:
if bc.check_password_hash(user.password, user_password):
has_valid_password = True

if not has_valid_password:
track_activity(
f"Failed MFA setup for user {user.user}. Invalid password.",
ctx_less=True,
display_in_ui=False,
)
return response_api_error('Invalid password')

user.mfa_secrets = mfa_secret
user.mfa_setup_complete = True
db.session.commit()

track_activity(
f"MFA setup successful for user {user.user}",
ctx_less=True,
display_in_ui=False,
)

return response_api_success({'mfa_setup_complete': True})

except ObjectNotFoundError:
return response_api_not_found()
except jwt.ExpiredSignatureError:
return response_api_error('Refresh token has expired')
except jwt.InvalidTokenError:
return response_api_error('Invalid refresh token')


@auth_blueprint.post('/mfa-verify')
def mfa_verify():
"""
Verify a TOTP token against the saved MFA secret.
Uses refresh_token to identify the user (no reliance on iris_current_user).
"""
data = request.get_json(silent=True) or {}

refresh_token = data.get('refresh_token')
token = data.get('token')

if not refresh_token or not token:
return response_api_error('Missing required fields: refresh_token, token')

try:
payload = jwt.decode(refresh_token, app.config.get('SECRET_KEY'), algorithms=['HS256'])

if payload.get('type') != 'refresh':
return response_api_error('Invalid token type')

user_id = payload.get('user_id')
user = users_get_active(user_id)

if not user.mfa_secrets or not user.mfa_setup_complete:
return response_api_error('MFA setup required')

totp = pyotp.TOTP(user.mfa_secrets)
if not totp.verify(str(token), valid_window=1):
track_activity(
f"Failed MFA verification for user {user.user}. Invalid token.",
ctx_less=True,
display_in_ui=False,
)
return response_api_error('Invalid token')

track_activity(
f"MFA verification successful for user {user.user}",
ctx_less=True,
display_in_ui=False,
)

return response_api_success({'mfa_verified': True})

except ObjectNotFoundError:
return response_api_not_found()
except jwt.ExpiredSignatureError:
return response_api_error('Refresh token has expired')
except jwt.InvalidTokenError:
return response_api_error('Invalid refresh token')


@auth_blueprint.get('/whoami')
def whoami():
"""
Expand Down
Loading