From 9de9d9701e88ec2ca2122acefe7924672e0860d2 Mon Sep 17 00:00:00 2001 From: Iliyan Angelov Date: Tue, 9 Dec 2025 17:07:38 +0200 Subject: [PATCH] updates --- .../__pycache__/auth_routes.cpython-312.pyc | Bin 36471 -> 41083 bytes .../__pycache__/user_routes.cpython-312.pyc | Bin 24001 -> 23210 bytes Backend/src/auth/routes/auth_routes.py | 189 ++++++++++++++---- Backend/src/auth/routes/user_routes.py | 30 +-- ...accountant_security_routes.cpython-312.pyc | Bin 13278 -> 13515 bytes .../routes/accountant_security_routes.py | 22 +- ...ccountant_security_service.cpython-312.pyc | Bin 11198 -> 11418 bytes .../services/accountant_security_service.py | 18 +- .../__pycache__/auth.cpython-312.pyc | Bin 9785 -> 9758 bytes .../__pycache__/step_up_auth.cpython-312.pyc | Bin 5923 -> 7150 bytes Backend/src/security/middleware/auth.py | 4 +- .../src/security/middleware/step_up_auth.py | 56 +++++- .../auth/components/StepUpAuthModal.tsx | 33 ++- .../services/accountantSecurityService.ts | 9 + 14 files changed, 270 insertions(+), 91 deletions(-) diff --git a/Backend/src/auth/routes/__pycache__/auth_routes.cpython-312.pyc b/Backend/src/auth/routes/__pycache__/auth_routes.cpython-312.pyc index a31ea7b2b7e887c862927b79bf0e87884767d3f9..4f2206e4efed9db0c08f1edf63422acb36430899 100644 GIT binary patch delta 13435 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 AoB#j- delta 9619 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 CFHxre diff --git a/Backend/src/auth/routes/__pycache__/user_routes.cpython-312.pyc b/Backend/src/auth/routes/__pycache__/user_routes.cpython-312.pyc index 6d21c29ab8105a82a14d5e841a0f976768abef04..a52f951a21f2f06806195f18dad05e56b916f39e 100644 GIT binary patch delta 4433 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 diff --git a/Backend/src/auth/routes/auth_routes.py b/Backend/src/auth/routes/auth_routes.py index f57f8f03..cc18eea4 100644 --- a/Backend/src/auth/routes/auth_routes.py +++ b/Backend/src/auth/routes/auth_routes.py @@ -2,6 +2,7 @@ from fastapi import APIRouter, Depends, HTTPException, status, Cookie, Response, from fastapi.responses import JSONResponse from sqlalchemy.orm import Session from pathlib import Path +from datetime import datetime import aiofiles import uuid import os @@ -13,6 +14,9 @@ from ...security.middleware.auth import get_current_user from ..models.user import User from ...analytics.services.audit_service import audit_service from ...shared.config.logging_config import get_logger +from ...payments.services.accountant_security_service import accountant_security_service +from ...auth.services.mfa_service import mfa_service +from ...shared.utils.role_helpers import is_admin from slowapi import Limiter, _rate_limit_exceeded_handler from slowapi.util import get_remote_address from slowapi.errors import RateLimitExceeded @@ -36,6 +40,99 @@ def get_limiter(request: Request) -> Limiter: limiter = request.app.state.limiter return limiter + +@router.post('/admin/step-up/verify') +async def verify_admin_step_up( + request: Request, + step_up_data: dict, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """ + Step-up verification for admins: accept password or MFA token. + Uses the accountant security session store but bypasses accountant role checks. + """ + if not is_admin(current_user, db): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='Forbidden') + + try: + from ...payments.models.accountant_session import AccountantSession + + mfa_token = step_up_data.get('mfa_token') + password = step_up_data.get('password') + session_token = step_up_data.get('session_token') + + if not session_token: + session_token = request.headers.get('X-Session-Token') or request.cookies.get('session_token') + + if not session_token: + active_session = db.query(AccountantSession).filter( + AccountantSession.user_id == current_user.id, + AccountantSession.is_active == True, + AccountantSession.expires_at > datetime.utcnow() + ).order_by(AccountantSession.last_activity.desc()).first() + + if active_session: + session_token = active_session.session_token + else: + new_session = accountant_security_service.create_session( + db=db, + user_id=current_user.id, + ip_address=request.client.host if request.client else None, + user_agent=request.headers.get('User-Agent') + ) + session_token = new_session.session_token + + if mfa_token: + try: + is_valid = mfa_service.verify_mfa(db, current_user.id, mfa_token) + if not is_valid: + raise HTTPException(status_code=401, detail='Invalid MFA token') + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + elif password: + import bcrypt + if not bcrypt.checkpw(password.encode('utf-8'), current_user.password.encode('utf-8')): + raise HTTPException(status_code=401, detail='Invalid password') + else: + raise HTTPException(status_code=400, detail='Either mfa_token or password is required') + + success = accountant_security_service.complete_step_up( + db=db, + session_token=session_token, + user_id=current_user.id + ) + + if not success: + raise HTTPException(status_code=400, detail='Failed to complete step-up authentication') + + client_ip = request.client.host if request.client else None + user_agent = request.headers.get('User-Agent') + + accountant_security_service.log_activity( + db=db, + user_id=current_user.id, + activity_type='admin_step_up_authentication', + activity_description='Admin step-up authentication completed', + ip_address=client_ip, + user_agent=user_agent, + risk_level='low', + metadata={'method': 'mfa' if mfa_token else 'password'} + ) + + db.commit() + + return JSONResponse( + status_code=status.HTTP_200_OK, + content={'status': 'success', 'data': {'step_up_completed': True}, 'message': 'Step-up authentication completed successfully'} + ) + except HTTPException: + raise + except Exception as e: + db.rollback() + logger.error(f'Error verifying admin step-up: {str(e)}', exc_info=True) + raise HTTPException(status_code=500, detail=str(e)) + def get_base_url(request: Request) -> str: return os.getenv('SERVER_URL') or f'http://{request.headers.get('host', 'localhost:8000')}' @@ -158,16 +255,16 @@ async def login( expected_role=login_request.expectedRole ) - # After successful login, check if user is accountant/admin and enforce MFA + # After successful login, check if user is accountant and enforce MFA requires_mfa_setup = False if result.get('user') and not result.get('requires_mfa'): user = db.query(User).filter(User.id == result['user']['id']).first() if user: try: from ...payments.services.accountant_security_service import accountant_security_service - from ...shared.utils.role_helpers import is_accountant, is_admin + from ...shared.utils.role_helpers import is_accountant - if is_accountant(user, db) or is_admin(user, db): + if is_accountant(user, db): # Check if MFA is required but not enabled is_enforced, reason = accountant_security_service.is_mfa_enforced(user, db) if not is_enforced: @@ -207,7 +304,7 @@ async def login( db=db, user_id=user.id, activity_type='login', - activity_description='Accountant/admin login successful', + activity_description='Accountant login successful', ip_address=client_ip, user_agent=user_agent, risk_level='low', @@ -255,15 +352,15 @@ async def login( ) return {'status': 'success', 'requires_mfa': True, 'user_id': result['user_id']} - # After successful login (MFA passed if required), check MFA for accountant/admin roles + # After successful login (MFA passed if required), check MFA for accountant role if not requires_mfa_setup: user = db.query(User).filter(User.id == result['user']['id']).first() if user: try: from ...payments.services.accountant_security_service import accountant_security_service - from ...shared.utils.role_helpers import is_accountant, is_admin + from ...shared.utils.role_helpers import is_accountant - if is_accountant(user, db) or is_admin(user, db): + if is_accountant(user, db): # Check if MFA is required but not enabled is_enforced, reason = accountant_security_service.is_mfa_enforced(user, db) if not is_enforced: @@ -281,36 +378,54 @@ async def login( status='success' ) logger.info(f'User {user.id} logged in but MFA setup required: {reason}') - else: - # MFA is enabled and enforced - create accountant session for tracking - try: - accountant_session = accountant_security_service.create_session( - db=db, - user_id=user.id, - ip_address=client_ip, - user_agent=user_agent, - device_fingerprint=None # Can be enhanced with device fingerprinting - ) - - # Log login activity - is_unusual = accountant_security_service.detect_unusual_activity( - db=db, - user_id=user.id, - ip_address=client_ip - ) - - accountant_security_service.log_activity( - db=db, - user_id=user.id, - activity_type='login', - activity_description='Accountant/admin login successful', - ip_address=client_ip, - user_agent=user_agent, - risk_level='low', - is_unusual=is_unusual - ) - except Exception as e: - logger.warning(f'Error creating accountant session: {e}') + # Always create an accountant security session so step-up auth + # works even if MFA is not yet enabled (password re-auth fallback). + try: + accountant_session = accountant_security_service.create_session( + db=db, + user_id=user.id, + ip_address=client_ip, + user_agent=user_agent, + device_fingerprint=None # Can be enhanced with device fingerprinting + ) + + # Commit the session to database so it's available for step-up auth + db.commit() + + # Store session_token in cookie for step-up authentication + from ...shared.config.settings import settings + session_max_age = 4 * 60 * 60 # 4 hours (matches ACCOUNTANT_SESSION_TIMEOUT_HOURS) + samesite_value = 'strict' if settings.is_production else 'lax' + response.set_cookie( + key='session_token', + value=accountant_session.session_token, + httponly=True, + secure=settings.is_production, + samesite=samesite_value, + max_age=session_max_age, + path='/' + ) + + # Log login activity + is_unusual = accountant_security_service.detect_unusual_activity( + db=db, + user_id=user.id, + ip_address=client_ip + ) + + accountant_security_service.log_activity( + db=db, + user_id=user.id, + activity_type='login', + activity_description='Accountant/admin login successful', + ip_address=client_ip, + user_agent=user_agent, + risk_level='low', + is_unusual=is_unusual + ) + except Exception as e: + db.rollback() + logger.warning(f'Error creating accountant session: {e}') except Exception as e: logger.warning(f'Error enforcing MFA for accountant: {e}') diff --git a/Backend/src/auth/routes/user_routes.py b/Backend/src/auth/routes/user_routes.py index 6140ffba..4fd9b164 100644 --- a/Backend/src/auth/routes/user_routes.py +++ b/Backend/src/auth/routes/user_routes.py @@ -5,7 +5,7 @@ from typing import Optional import bcrypt from ...shared.config.database import get_db from ...security.middleware.auth import get_current_user, authorize_roles -from ...security.middleware.step_up_auth import require_step_up_auth +from ...security.middleware.step_up_auth import require_step_up_auth, require_admin_step_up_auth from ..models.user import User from ..models.role import Role from ...bookings.models.booking import Booking, BookingStatus @@ -57,7 +57,7 @@ async def get_user_by_id(id: int, current_user: User=Depends(authorize_roles('ad except Exception as e: raise HTTPException(status_code=500, detail=str(e)) -@router.post('/', dependencies=[Depends(authorize_roles('admin')), Depends(require_step_up_auth("user creation"))]) +@router.post('/', dependencies=[Depends(authorize_roles('admin')), Depends(require_admin_step_up_auth("user creation"))]) async def create_user( request: Request, user_data: CreateUserRequest, @@ -147,7 +147,7 @@ async def create_user( db.rollback() raise HTTPException(status_code=500, detail=str(e)) -@router.put('/{id}') +@router.put('/{id}', dependencies=[Depends(authorize_roles('admin')), Depends(require_admin_step_up_auth("user management"))]) async def update_user( id: int, request: Request, @@ -164,28 +164,8 @@ async def update_user( if not can_manage_users(current_user, db) and current_user.id != id: raise HTTPException(status_code=403, detail='Forbidden') - # SECURITY: Require step-up auth for admin user management operations - is_admin_managing_user = can_manage_users(current_user, db) and current_user.id != id - if is_admin_managing_user: - # Check if step-up auth is required (this will raise if not authenticated) - from ...payments.services.accountant_security_service import accountant_security_service - session_token = request.headers.get('X-Session-Token') or request.cookies.get('session_token') - requires_step_up, reason = accountant_security_service.require_step_up( - db=db, - user_id=current_user.id, - session_token=session_token, - action_description="user management" - ) - if requires_step_up: - from fastapi import status - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail={ - 'error': 'step_up_required', - 'message': reason or 'Step-up authentication required for user management operations', - 'action': 'user_management' - } - ) + # Step-up is no longer enforced for admins via accountant security. + # If a separate admin step-up is desired, wire it here instead. user = db.query(User).options(joinedload(User.role)).filter(User.id == id).first() if not user: diff --git a/Backend/src/payments/routes/__pycache__/accountant_security_routes.cpython-312.pyc b/Backend/src/payments/routes/__pycache__/accountant_security_routes.cpython-312.pyc index d6e4afe857f86b081d80d7250a978640ee5a1532..a1b30e7d44b39bc5cedc57112116126483d84498 100644 GIT binary patch delta 3189 zcmb7FZERat8NTOUe}5;A6ebb+qv0Z8{6h$#v_%s_LU7(|U)$-j zA6UBYdC&X4=ly)n@toF&4$*{tVib3Z z9oR2gauyt*)2P=MTt{$-<9JRK*w===wG)2b(ttzPjq5cd?zNuPfB1b}Uv$VF7JC~Z zOt_nd_+DS+i>&R<5K+3!_kK0jVrj&29N*|!=WCH!YDV14WPKa`>q3hxvFl9huC<)* zqzUh1xec%{DcZzDkh#G78Mja4^h_Lhke(2FeFs{+*8B`%FAbr1P`5PTR7+cuONr`7 z!YZO?jA!_JCOX{ZrFV^gHr+(Bw=B@#_WG)8renyNSNiLn%CIiScI?=2>b<{)im#PTWD>ivrO;JzlpXn_kh@^YkJ7cUH}27MDD(N_ zx{u~svwEpvrTdjbyCSx=Mr_fzaQiMzvxDPW2ZybpA+L^!inHw8Rf!9Cw4R0zL$8Pb z&t6wt&~1OC+ZA`qBu~^Q>9=4X?ku~?ZhFWTMbFZ$u8;w7%4u4(MOQ}jj`VS)t731h zP&usY%67e%tyyDB#lCD`M2p;;!g+2H^>WG~{RH#IBkmffJl7(buGq^${wul!R{F;- zmy0EHMi=I%V0h;vdj-GEeV@nXji^2#G;Zk zkS-QoHP16qTEePLnoDOeCZa5>HcglTJ9^a-2;b#ZyR2lCc{!aeVNo?3+H#yK6>62MiIK;+K+nZn(M_e z?Q}cStF9~&Gm4m&St?SgsG70`iDjZ1E|p}3bbyI%K_pUcDLtFZ5F_|>x(88=j=MYQ zMfb#thXk0Az;i-EK(s_6z(s+?fIvXimB}j7LNk`VC@wYvTO>KXkSVa{AdyUhxmlji zW}!pYT25(cPOC}wFl|i#(RPpQyTBxntNa;zHvh`~>ngY7d4+!9$?}hE^uL}PQ<0+& zBF}F{p5OKdx5EQ>&fYq^ePryX#gB^bFWyK!BH|;wPE`H!a?C4hP=uKz!-L{{h7pCIq`g8Wlaqgby(4?RHC3mWmZ8kd* zQ*8RJlcU0BtaI{+uzAD?{HOrh@%E{R@Egxb>r_zqBxq!tPa?upyn5R!7zVmo1usu0 zZDfow%^9lZY-zrzkR{cSm6Rp&487+cU727us--?Q`7R=G&8`Gb6H+3voTy|kNyV9% zu0IwdUjnB`fWSkN>>^(VUYpY>aPWX>h^mDjhwVuq&j5Lr5mm?(3aTkHHzyW{aWg^k zI#{H^!Yaw7T#3wP6!HcLGe9l^nV>%o1ZLn8Y9LCQp9D*msZ>Wj;dDWg6^4KsD#lR1XE+2GmVYbcazt{d#x*t4}fUl|ZdKL|iCcZt2dk z*vrlCOeZ~ic1D{CTsU$H2wVg*PPe;z&Ol8`6S)kMCoX^lGA(%%<7&y*a(8J-H0}bK zW%~E3ry_sn(J&p3_t{&ab&Xs6E$2t-D?gIBG_3P8~{V$#)sH*MVGRM70!3Gc{xjj3ejh_mY8b4a#i|C_{Lr zl*tWt;@8!!WRy3n#tfd7iq)OLzajJ%{lied>1}X|sVNl?v>=5Er;`o>FFSdMPNeoT z*sub^%>r7l-8*3R9r}Y*Ain1XBdcHl#DJ;~QZMi?T>!z;K;W9Gg2bLBK&}QU0#Q`M z#ZsxDC9j>l%ROdNPDA6rEyrg|c)lQxlOHh|VxRmfT^P=-gzv}4?*}J--yVEFK@UE( zUO?#hwzFeL0Or&7&K)B#J0{NS-!W^fg|m7dX?BkVj*so|nsk}xc+0m!-wfTDypIBZ y3C7lf5zdiDLgP(Znw&8~?<^KWp>c{E; delta 2915 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 T$igW*YCLo&arichT8;h%1fD{E diff --git a/Backend/src/payments/services/accountant_security_service.py b/Backend/src/payments/services/accountant_security_service.py index aa9dab89..b8a6e5c9 100644 --- a/Backend/src/payments/services/accountant_security_service.py +++ b/Backend/src/payments/services/accountant_security_service.py @@ -6,7 +6,7 @@ from typing import Optional, Dict, Any, Tuple from datetime import datetime, timedelta from ...auth.models.user import User from ..models.accountant_session import AccountantSession, AccountantActivityLog -from ...shared.utils.role_helpers import is_accountant, is_admin +from ...shared.utils.role_helpers import is_accountant from ...shared.config.logging_config import get_logger import secrets import hashlib @@ -27,10 +27,8 @@ class AccountantSecurityService: @staticmethod def requires_mfa(user: User, db: Session) -> bool: """Check if user role requires MFA.""" - # Admin and all accountant roles require MFA - if is_admin(user, db) or is_accountant(user, db): - return True - return False + # Only accountant roles are handled by this service + return is_accountant(user, db) @staticmethod def is_mfa_enforced(user: User, db: Session) -> Tuple[bool, Optional[str]]: @@ -40,7 +38,7 @@ class AccountantSecurityService: """ if AccountantSecurityService.requires_mfa(user, db): if not user.mfa_enabled: - return False, "MFA is required for accountant/admin roles but not enabled" + return False, "MFA is required for accountant role but not enabled" return True, None return False, None @@ -120,12 +118,18 @@ class AccountantSecurityService: db: Session, user_id: int, session_token: Optional[str] = None, - action_description: str = "high-risk action" + action_description: str = "high-risk action", + enforce_role_check: bool = True, ) -> Tuple[bool, Optional[str]]: """ Check if step-up authentication is required. Returns (requires_step_up: bool, reason: str | None) """ + user = db.query(User).filter(User.id == user_id).first() + if enforce_role_check: + # Only enforce step-up for accountant roles when role check is enabled + if user and not is_accountant(user, db): + return False, None # If no session token provided, try to find the most recent active session for this user if not session_token: active_session = db.query(AccountantSession).filter( diff --git a/Backend/src/security/middleware/__pycache__/auth.cpython-312.pyc b/Backend/src/security/middleware/__pycache__/auth.cpython-312.pyc index 89d047de2e4256ce93d5eb9ca6d9f10e3b8b9049..fd373ec4614972392cf74e950e81c6bd35608335 100644 GIT binary patch delta 113 zcmdn#GtY@BfDSK)$oG=mgRA)ZPF72trd{T)O#MTn>GGIPwqv@s1 Pd`g?u%V2Yu#A99n8?+x< delta 119 zcmbQ|v(ty~G%qg~0}$MDFwSI9-^jOwi>ZQj@@iRw%}=--nAwUsnHXyLHcRk%voo&T zoFY1(k#k|jijoaMSJW&vGmG;u>eww6O4(|_>V;74?ki|Hb(+9n4sYg{~%_Qkq}~x=e)Kuj5m3|d+#~t z-gED{zjNNX=pQQ_KicgU0AOi5BqTC<*j>>JW0?MKCddYF4Ea|g^DlFp-Kz@Kz!7Bxrp1C68Zuyk;>gWfhJg-jtL^n{{!zE!Me@U=Oy0WMFm=XDi&?Xt?%gSTWd@KZ7%vDWB zyHIsj?u)u2&Di)op+j<3HL9yR%D8z{zm`7YBl?AlJs>R^3yREQ=_K>PjG3p)P(_yx{#D1P^~F4-b<$ z8*w$A+I8`%>&_@o7{Ytds9C z?&LG{eR`fB*L7Rs$y_?$F`Uni@~b-ktNn_s1l5!rRN^@RIC@86B$$&EsdzrEz*8m;fp_>RL?~)`<>arw9 z#{?uxQ7F=)?o#Cu6$t%S!gmOfTA@mz161)-QY0A@-z9{s`l@_P&%YN|`1Qw(N%6Ab zk_f(_?Sh@g#4$rrWbOQlIef@iG{6obkkoX{c$$2m1AFrs;j*Rb^9V(sp9rtfu=PcT z%m~wx2`PtBVs#+UUUq$0Wcc2!zA7odwOh^)4YPzC!*Im3aIgk6@gp2+L<|Cit)+S; z6$dgz3u-#6L^b=AoE?#w!j_>~3qmVG8-g979U+9EN5E^cMF3&bf1|B!p>Z&bMvD<# zI8;mk_&(4MYdq12b8uOeEZR+Zy`g=}dNltOpiIFB7GhrYtF!Ku?Zd(A&hX9Jz@#m3 z#}_#3e9w8q7n$-!CYP+9_Vxbk8@d|$uJfDD8-4v#ef^W$<>|hmH|=*@dih5Nulr`8 zbC6xxo;5a$jkjxjGXnH?y$v%W^m7Jc_spPX&O|JZ2WAHhXMmYD|D?Ww zbxnL>G|}zle1wfP@+K<0ggGG?MGoJ62e7%!Wwlb~2=KE=tBe8`ldVJ*a1pkQo=41` z)_==U#th}jqX$Y`_^eCP>56N+dLlf-8n)w}z&lH4V7|V!gjnEKZ zYLz-PO6V))1k@!fFu20Zq{amW`GXW-5w2L*HRNHcTmUE+8_T?6Q?i~Ggg%iUumx<- zqZwWk*PXL15Had${)25D?V51dw^Hl8knyaeo?e}J!*Po`7*mnYTFbOT1X}n-m(7ZN zF3ZCFYu8e`7((zF4f69fTZWzn1zUyi3_=gWYJhF57clH)%Tclkpya~B7R?PO8 z7f@_W#g(|KGPW6K`aBf0lDNMhv(b`aD4kVh#zK6*yO}QHZ@HyGEyLD;DO-!shwvOg z*!(z>n!oby4z1E_d5b%1PS`qJq$S%%7%5mlFv{2<*|@Ft6~o)87~r|uDZasTqNNL0 zV5Wt24A-!osDFa~JW|`>WCI{SfjSJrLq28m@@^?ayZCOYxsD$6tEsdSOr`gxI#^oS z7c8f-D%~<@adPk--i4d-7gjRGVtC+KOuJ`EP#{8`pkSCw3}009HBOjzHvE-5GW9@o z3t73B?S&~aK*{oJ-jMJM>E`#mfym5mN&-EzLWufr`uz8?|8pB%Kz%bC=}OvqQJEvq HX*>P{uvp~+ delta 1219 zcmZvbO>7%Q6vt=w!?C^g+G~6L5eLWlj_cqwq)I{w{YazY1C%grKORGI3V7vn-(G7;kPsI zy?Hb9pSL%s-_5Dt`2Ajj);=*=-H&amar*J*h?TEL3K2>S*-&bFJz9uTA`mlb#@?qq znUF%Jq30zn6W+YJ(=D|9RjXJkmsgt&yVS7T zZay{Y)yDRB^f5}$K}_ryRa=BXu@A;Qqfi$^)D4F{BZ_da;RO-i^i0FAVmJJb8CfYu z*PtNvM|3<*yQ^uLtXMTz9A`{}i;^y^4Q}3!!Do`6X5d?C!mTis2LrFm=an!YML2X= zk_V^;8}fU-pQ2;`7qY)j_IyN3y-~Y7w;LS4I`hfnU-j=!o`K`8EAr6XDAxPU)vIR& zrtu1g5SD;gO0x{~b#>D$%yunqU-d~;>FmMNqHr=CaRCnR48wJQ+PQFAkAy6tMa~ji z#)xApE(k^!bX{~ELfpNDn#C1PO36&03L9bQvI zj$U7@d)XxBvK~Y`#LVZLRc2bn`f{mgnRfFzmcl^hKqI&j$kHJQ1oa1pxnH^~7GJF3 z>zR$^RaQ0)Hi{lgLZs2~`U}DNfqCxx5Yds+`+rh?6ucw`<}^5?_4*#+`Mj@qffuzt z`B5AnhqtwPI<|d7JNsW16ZG2l)8QYeWclm?Nb50W2p2fEGW;*?G=-n8x+*+WhCqDB^Fd{+^~ICBxjg(7 ze>FA67tSKEeffI!1kb$JzON+G@&bCwu$4HCdi|M5!IADA15y#N3J diff --git a/Backend/src/security/middleware/auth.py b/Backend/src/security/middleware/auth.py index 822d43a6..6d6dd054 100644 --- a/Backend/src/security/middleware/auth.py +++ b/Backend/src/security/middleware/auth.py @@ -123,7 +123,7 @@ def get_current_user( detail=f'Account is temporarily locked due to multiple failed login attempts. Please try again in {remaining_minutes} minute(s).' ) - # SECURITY: Check MFA for accountant/admin roles (warn but allow access for MFA setup) + # SECURITY: Check MFA for accountant roles (warn but allow access for MFA setup) # Note: MFA enforcement for financial endpoints is handled by route-level dependencies # This check only logs warnings to allow users to access MFA setup pages try: @@ -133,7 +133,7 @@ def get_current_user( logger = get_logger(__name__) - if is_accountant(user, db) or is_admin(user, db): + if is_accountant(user, db): is_enforced, reason = accountant_security_service.is_mfa_enforced(user, db) if not is_enforced and reason: # Log warning but allow access so user can set up MFA diff --git a/Backend/src/security/middleware/step_up_auth.py b/Backend/src/security/middleware/step_up_auth.py index 77adc1b3..78a0c8f5 100644 --- a/Backend/src/security/middleware/step_up_auth.py +++ b/Backend/src/security/middleware/step_up_auth.py @@ -8,7 +8,7 @@ from ...shared.config.database import get_db from ...security.middleware.auth import get_current_user from ...auth.models.user import User from ...payments.services.accountant_security_service import accountant_security_service -from ...shared.utils.role_helpers import is_accountant, is_admin +from ...shared.utils.role_helpers import is_accountant from ...shared.config.logging_config import get_logger logger = get_logger(__name__) @@ -25,8 +25,8 @@ def require_step_up_auth( current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ) -> User: - # Only enforce for accountant/admin roles - if not (is_accountant(current_user, db) or is_admin(current_user, db)): + # Only enforce for accountant roles; admins are handled separately + if not is_accountant(current_user, db): return current_user # Regular users don't need step-up # Get session token from request @@ -60,6 +60,48 @@ def require_step_up_auth( return step_up_checker +def require_admin_step_up_auth( + action_description: str = "this high-risk admin action" +): + """ + Dependency to require step-up authentication for admin-only high-risk operations. + Uses the same step-up mechanism but bypasses accountant role checks. + """ + async def step_up_checker( + request: Request, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) + ) -> User: + from ...shared.utils.role_helpers import is_admin + + if not is_admin(current_user, db): + return current_user # Only admins are subject to this dependency + + session_token = request.headers.get('X-Session-Token') or request.cookies.get('session_token') + + requires_step_up, reason = accountant_security_service.require_step_up( + db=db, + user_id=current_user.id, + session_token=session_token, + action_description=action_description, + enforce_role_check=False, # allow admin + ) + + if requires_step_up: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail={ + 'error': 'step_up_required', + 'message': reason or f'Step-up authentication required for {action_description}', + 'action': action_description + } + ) + + return current_user + + return step_up_checker + + def enforce_mfa_for_accountants(): """ Dependency to enforce MFA for accountant/admin roles. @@ -69,8 +111,8 @@ def enforce_mfa_for_accountants(): current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ) -> User: - # Only enforce for accountant/admin roles - if not (is_accountant(current_user, db) or is_admin(current_user, db)): + # Only enforce for accountant roles; admins are handled separately + if not is_accountant(current_user, db): return current_user # Regular users don't need MFA # Check if MFA is required and enforced @@ -131,8 +173,8 @@ def authorize_financial_access(*allowed_roles: str): detail='You do not have permission to access this resource' ) - # Then enforce MFA for accountant/admin roles - if is_accountant(current_user, db) or is_admin(current_user, db): + # Then enforce MFA only for accountant roles (admins handled elsewhere) + if is_accountant(current_user, db): is_enforced, reason = accountant_security_service.is_mfa_enforced(current_user, db) if not is_enforced and reason: diff --git a/Frontend/src/features/auth/components/StepUpAuthModal.tsx b/Frontend/src/features/auth/components/StepUpAuthModal.tsx index f80de1cd..59824272 100644 --- a/Frontend/src/features/auth/components/StepUpAuthModal.tsx +++ b/Frontend/src/features/auth/components/StepUpAuthModal.tsx @@ -38,6 +38,7 @@ const StepUpAuthModal: React.FC = ({ actionDescription = 'this action', }) => { const { userInfo } = useAuthStore(); + const isAdmin = (userInfo?.role || (userInfo as any)?.role_name)?.toLowerCase() === 'admin'; const [verificationMethod, setVerificationMethod] = useState<'mfa' | 'password'>('mfa'); const [isVerifying, setIsVerifying] = useState(false); const [error, setError] = useState(null); @@ -92,9 +93,13 @@ const StepUpAuthModal: React.FC = ({ setIsVerifying(true); setError(null); - const response = await accountantSecurityService.verifyStepUp({ + const response = await (isAdmin + ? accountantSecurityService.verifyAdminStepUp({ + mfa_token: data.mfaToken, + }) + : accountantSecurityService.verifyStepUp({ mfa_token: data.mfaToken, - }); + })); if (response.status === 'success' && response.data.step_up_completed) { toast.success('Identity verified successfully'); @@ -106,10 +111,16 @@ const StepUpAuthModal: React.FC = ({ throw new Error('Step-up verification failed'); } } catch (error: any) { + // Prevent page refresh by ensuring error is caught and handled const errorMessage = - error.response?.data?.detail || error.response?.data?.message || 'Failed to verify identity. Please try again.'; + error.response?.data?.detail || + (typeof error.response?.data === 'string' ? error.response.data : null) || + error.response?.data?.message || + error.message || + 'Failed to verify identity. Please try again.'; setError(errorMessage); toast.error(errorMessage); + // Don't close modal on error - let user try again } finally { setIsVerifying(false); } @@ -120,9 +131,13 @@ const StepUpAuthModal: React.FC = ({ setIsVerifying(true); setError(null); - const response = await accountantSecurityService.verifyStepUp({ + const response = await (isAdmin + ? accountantSecurityService.verifyAdminStepUp({ + password: data.password, + }) + : accountantSecurityService.verifyStepUp({ password: data.password, - }); + })); if (response.status === 'success' && response.data.step_up_completed) { toast.success('Identity verified successfully'); @@ -134,10 +149,16 @@ const StepUpAuthModal: React.FC = ({ throw new Error('Step-up verification failed'); } } catch (error: any) { + // Prevent page refresh by ensuring error is caught and handled const errorMessage = - error.response?.data?.detail || error.response?.data?.message || 'Invalid password. Please try again.'; + error.response?.data?.detail || + (typeof error.response?.data === 'string' ? error.response.data : null) || + error.response?.data?.message || + error.message || + 'Invalid password. Please try again.'; setError(errorMessage); toast.error(errorMessage); + // Don't close modal on error - let user try again } finally { setIsVerifying(false); } diff --git a/Frontend/src/features/security/services/accountantSecurityService.ts b/Frontend/src/features/security/services/accountantSecurityService.ts index 06756052..5945e59d 100644 --- a/Frontend/src/features/security/services/accountantSecurityService.ts +++ b/Frontend/src/features/security/services/accountantSecurityService.ts @@ -45,6 +45,15 @@ class AccountantSecurityService { return response.data; } + async verifyAdminStepUp(data: { + mfa_token?: string; + password?: string; + session_token?: string; + }): Promise<{ status: string; data: { step_up_completed: boolean } }> { + const response = await apiClient.post('/auth/admin/step-up/verify', data); + return response.data; + } + async getSessions(): Promise<{ status: string; data: { sessions: AccountantSession[] } }> { const response = await apiClient.get('/accountant/security/sessions'); return response.data;