Brak opisu

login_routes.py 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. # IRIS Source Code
  2. # Copyright (C) 2021 - Airbus CyberSecurity (SAS)
  3. # ir@cyberactionlab.net
  4. #
  5. # This program is free software; you can redistribute it and/or
  6. # modify it under the terms of the GNU Lesser General Public
  7. # License as published by the Free Software Foundation; either
  8. # version 3 of the License, or (at your option) any later version.
  9. #
  10. # This program is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  13. # Lesser General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU Lesser General Public License
  16. # along with this program; if not, write to the Free Software Foundation,
  17. # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. import base64
  19. import io
  20. import pyotp
  21. import qrcode
  22. import random
  23. import string
  24. from flask import Blueprint, flash
  25. from flask import redirect
  26. from flask import render_template
  27. from flask import request
  28. from flask import session
  29. from flask import url_for
  30. from flask_login import current_user
  31. from oic import rndstr
  32. from oic.oic.message import AuthorizationResponse
  33. from app import app
  34. from app import bc
  35. from app import db
  36. from app import oidc_client
  37. from app.blueprints.access_controls import is_authentication_oidc, is_authentication_ldap
  38. from app.blueprints.responses import response_error
  39. from app.business.auth import validate_ldap_login, _retrieve_user_by_username, wrap_login_user
  40. from app.datamgmt.manage.manage_users_db import create_user
  41. from app.datamgmt.manage.manage_users_db import get_user
  42. from app.forms import LoginForm, MFASetupForm
  43. from app.iris_engine.utils.tracker import track_activity
  44. login_blueprint = Blueprint(
  45. 'login',
  46. __name__,
  47. template_folder='templates'
  48. )
  49. log = app.logger
  50. # filter User out of database through username
  51. def _render_template_login(form, msg):
  52. organisation_name = app.config.get('ORGANISATION_NAME')
  53. login_banner = app.config.get('LOGIN_BANNER_TEXT')
  54. ptfm_contact = app.config.get('LOGIN_PTFM_CONTACT')
  55. auth_type = app.config.get('AUTHENTICATION_TYPE')
  56. return render_template('login.html', form=form, msg=msg, organisation_name=organisation_name,
  57. login_banner=login_banner, ptfm_contact=ptfm_contact, auth_type=auth_type)
  58. def _validate_local_login(username, password):
  59. user = _retrieve_user_by_username(username)
  60. if not user:
  61. return None
  62. if bc.check_password_hash(user.password, password):
  63. return user
  64. track_activity(f'wrong login password for user \'{username}\' using local auth', ctx_less=True, display_in_ui=False)
  65. return None
  66. def _authenticate_ldap(form, username, password, local_fallback=True):
  67. try:
  68. user = validate_ldap_login(username, password, local_fallback)
  69. if user is None:
  70. return _render_template_login(form, 'Wrong credentials. Please try again.')
  71. return wrap_login_user(user)
  72. except Exception as e:
  73. log.error(e.__str__())
  74. return _render_template_login(form, 'LDAP authentication unavailable. Check server logs')
  75. def _authenticate_password(form, username, password):
  76. user = _retrieve_user_by_username(username)
  77. if not user or user.is_service_account:
  78. return _render_template_login(form, 'Wrong credentials. Please try again.')
  79. if bc.check_password_hash(user.password, password):
  80. return wrap_login_user(user)
  81. track_activity(f'wrong login password for user \'{username}\' using local auth', ctx_less=True,
  82. display_in_ui=False)
  83. return _render_template_login(form, 'Wrong credentials. Please try again.')
  84. # Authenticate user
  85. if app.config.get("AUTHENTICATION_TYPE") in ["local", "ldap", "oidc"]:
  86. @login_blueprint.route('/login', methods=['GET', 'POST'])
  87. def login():
  88. if current_user.is_authenticated:
  89. return redirect(url_for('index.index'))
  90. if is_authentication_oidc() and app.config.get('AUTHENTICATION_LOCAL_FALLBACK') is False:
  91. return redirect(url_for('login.oidc_login'))
  92. form = LoginForm(request.form)
  93. # check if both http method is POST and form is valid on submit
  94. if not form.is_submitted() and not form.validate():
  95. return _render_template_login(form, None)
  96. # assign form data to variables
  97. username = request.form.get('username', '', type=str)
  98. password = request.form.get('password', '', type=str)
  99. if is_authentication_ldap() is True:
  100. return _authenticate_ldap(form, username, password, app.config.get('AUTHENTICATION_LOCAL_FALLBACK'))
  101. return _authenticate_password(form, username, password)
  102. if is_authentication_oidc():
  103. @login_blueprint.route('/oidc-login')
  104. def oidc_login():
  105. if current_user.is_authenticated:
  106. return redirect(url_for('index.index'))
  107. session["oidc_state"] = rndstr()
  108. session["oidc_nonce"] = rndstr()
  109. args = {
  110. "client_id": oidc_client.client_id,
  111. "response_type": "code",
  112. "scope": app.config.get("OIDC_SCOPES"),
  113. "nonce": session["oidc_nonce"],
  114. "redirect_uri": url_for("login.oidc_authorise", _external=True),
  115. "state": session["oidc_state"]
  116. }
  117. auth_req = oidc_client.construct_AuthorizationRequest(request_args=args)
  118. login_url = auth_req.request(oidc_client.authorization_endpoint)
  119. return redirect(login_url)
  120. if is_authentication_oidc():
  121. @login_blueprint.route('/oidc-authorize')
  122. def oidc_authorise():
  123. auth_resp = oidc_client.parse_response(AuthorizationResponse, info=request.args,
  124. sformat="dict")
  125. if auth_resp["state"] != session["oidc_state"]:
  126. track_activity(
  127. f"OIDC session state '{auth_resp['state']}' does not match authorization state '{session['oidc_state']}'",
  128. ctx_less=True,
  129. display_in_ui=False,
  130. )
  131. return redirect(url_for("login.login"))
  132. args = {
  133. "code": auth_resp["code"],
  134. }
  135. access_token_resp = oidc_client.do_access_token_request(state=auth_resp["state"], request_args=args)
  136. # not all providers set email by default, use preferred_username where it's missing
  137. # Use the mapping from the configuration or default to email or preferred_username if not set
  138. email_field = app.config.get("OIDC_MAPPING_EMAIL")
  139. username_field = app.config.get("OIDC_MAPPING_USERNAME")
  140. user_login = access_token_resp['id_token'].get(username_field) or access_token_resp['id_token'].get(email_field)
  141. user_name = access_token_resp['id_token'].get(email_field) or access_token_resp['id_token'].get(username_field)
  142. user = get_user(user_login, 'user')
  143. if not user:
  144. log.warning(f'OIDC user {user_login} not found in database')
  145. if app.config.get('AUTHENTICATION_CREATE_USER_IF_NOT_EXIST') is False:
  146. log.warning('Authentication is set to not create user if not exists')
  147. track_activity(
  148. f'OIDC user {user_login} not found in database',
  149. ctx_less=True,
  150. display_in_ui=False,
  151. )
  152. return response_error('User not found in IRIS', 404)
  153. log.info(f'Creating OIDC user {user_login} in database')
  154. track_activity(
  155. f'Creating OIDC user {user_login} in database',
  156. ctx_less=True,
  157. display_in_ui=False,
  158. )
  159. # generate random password
  160. password = ''.join(random.choices(string.printable[:-6], k=16))
  161. user = create_user(
  162. user_name=user_name,
  163. user_login=user_login,
  164. user_email=user_login,
  165. user_password=bc.generate_password_hash(password.encode('utf8')).decode('utf8'),
  166. user_active=True,
  167. user_is_service_account=False
  168. )
  169. if user and not user.active:
  170. return response_error("User not active in IRIS", 403)
  171. return wrap_login_user(user, is_oidc=True)
  172. @app.route('/auth/mfa-setup', methods=['GET', 'POST'])
  173. def mfa_setup():
  174. user = _retrieve_user_by_username(username=session['username'])
  175. form = MFASetupForm()
  176. if form.submit() and form.validate():
  177. token = form.token.data
  178. mfa_secret = form.mfa_secret.data
  179. user_password = form.user_password.data
  180. totp = pyotp.TOTP(mfa_secret)
  181. if totp.verify(token):
  182. has_valid_password = False
  183. if is_authentication_ldap() is True:
  184. if validate_ldap_login(user.user, user_password,
  185. local_fallback=app.config.get('AUTHENTICATION_LOCAL_FALLBACK')):
  186. has_valid_password = True
  187. elif bc.check_password_hash(user.password, user_password):
  188. has_valid_password = True
  189. if not has_valid_password:
  190. track_activity(f'Failed MFA setup for user {user.user}. Invalid password.', ctx_less=True, display_in_ui=False)
  191. flash('Invalid password. Please try again.', 'danger')
  192. return render_template('mfa_setup.html', form=form)
  193. user.mfa_secrets = mfa_secret
  194. user.mfa_setup_complete = True
  195. db.session.commit()
  196. session["mfa_verified"] = False
  197. track_activity(f'MFA setup successful for user {user.user}', ctx_less=True, display_in_ui=False)
  198. return wrap_login_user(user)
  199. else:
  200. track_activity(f'Failed MFA setup for user {user.user}. Invalid token.', ctx_less=True, display_in_ui=False)
  201. flash('Invalid token or password. Please try again.', 'danger')
  202. temp_otp_secret = pyotp.random_base32()
  203. otp_uri = pyotp.TOTP(temp_otp_secret).provisioning_uri(user.email, issuer_name="IRIS")
  204. form.mfa_secret.data = temp_otp_secret
  205. img = qrcode.make(otp_uri)
  206. buf = io.BytesIO()
  207. img.save(buf, format='PNG')
  208. img_str = base64.b64encode(buf.getvalue()).decode()
  209. return render_template('mfa_setup.html', form=form, img_data=img_str, otp_setup_key=temp_otp_secret)
  210. @app.route('/auth/mfa-verify', methods=['GET', 'POST'])
  211. def mfa_verify():
  212. if 'username' not in session:
  213. return redirect(url_for('login.login'))
  214. user = _retrieve_user_by_username(username=session['username'])
  215. # Redirect user to MFA setup if MFA is not fully set up
  216. if not user.mfa_secrets or not user.mfa_setup_complete:
  217. track_activity(f'MFA setup required for user {user.user}', ctx_less=True, display_in_ui=False)
  218. return redirect(url_for('mfa_setup'))
  219. form = MFASetupForm()
  220. form.user_password.data = 'not required for verification'
  221. if form.submit() and form.validate():
  222. token = form.token.data
  223. if not token:
  224. flash('Token is required.', 'danger')
  225. return render_template('mfa_verify.html', form=form)
  226. totp = pyotp.TOTP(user.mfa_secrets)
  227. if totp.verify(token):
  228. session.pop('username', None)
  229. session['mfa_verified'] = True
  230. track_activity(f'MFA verification successful for user {user.user}', ctx_less=True, display_in_ui=False)
  231. return wrap_login_user(user)
  232. else:
  233. track_activity(f'Failed MFA verification for user {user.user}. Invalid token.', ctx_less=True, display_in_ui=False)
  234. flash('Invalid token. Please try again.', 'danger')
  235. return render_template('mfa_verify.html', form=form)