You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

166 lines
6.6 KiB

  1. # -*- coding: utf-8 -*-
  2. # Copyright 2016-2017 LasLabs Inc.
  3. # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
  4. from datetime import datetime, timedelta
  5. import json
  6. from werkzeug.contrib.securecookie import SecureCookie
  7. from werkzeug.wrappers import Response as WerkzeugResponse
  8. from odoo import _, http, registry, SUPERUSER_ID
  9. from odoo.api import Environment
  10. from odoo.http import Response, request
  11. from odoo.addons.web.controllers.main import Home
  12. from ..exceptions import MfaTokenInvalidError, MfaTokenExpiredError
  13. class JsonSecureCookie(SecureCookie):
  14. serialization_method = json
  15. class AuthTotp(Home):
  16. @http.route()
  17. def web_login(self, *args, **kwargs):
  18. """Add MFA logic to the web_login action in Home
  19. Overview:
  20. * Call web_login in Home
  21. * Return the result of that call if the user has not logged in yet
  22. using a password, does not have MFA enabled, or has a valid
  23. trusted device cookie
  24. * If none of these is true, generate a new MFA login token for the
  25. user, log the user out, and redirect to the MFA login form
  26. """
  27. # sudo() is required because there may be no request.env.uid (likely
  28. # since there may be no user logged in at the start of the request)
  29. user_model_sudo = request.env['res.users'].sudo()
  30. config_model_sudo = user_model_sudo.env['ir.config_parameter']
  31. response = super(AuthTotp, self).web_login(*args, **kwargs)
  32. if not request.params.get('login_success'):
  33. return response
  34. user = user_model_sudo.browse(request.uid)
  35. if not user.mfa_enabled:
  36. return response
  37. cookie_key = 'trusted_devices_%d' % user.id
  38. device_cookie = request.httprequest.cookies.get(cookie_key)
  39. if device_cookie:
  40. secret = config_model_sudo.get_param('database.secret')
  41. device_cookie = JsonSecureCookie.unserialize(device_cookie, secret)
  42. if device_cookie.get('device_id') in user.trusted_device_ids.ids:
  43. return response
  44. user.generate_mfa_login_token()
  45. request.session.logout(keep_db=True)
  46. request.params['login_success'] = False
  47. return http.local_redirect(
  48. '/auth_totp/login',
  49. query={
  50. 'mfa_login_token': user.mfa_login_token,
  51. 'redirect': request.params.get('redirect'),
  52. },
  53. keep_hash=True,
  54. )
  55. @http.route(
  56. '/auth_totp/login',
  57. type='http',
  58. auth='public',
  59. methods=['GET'],
  60. website=True,
  61. )
  62. def mfa_login_get(self, *args, **kwargs):
  63. return request.render('auth_totp.mfa_login', qcontext=request.params)
  64. @http.route('/auth_totp/login', type='http', auth='none', methods=['POST'])
  65. def mfa_login_post(self, *args, **kwargs):
  66. """Process MFA login attempt
  67. Overview:
  68. * Try to find a user based on the MFA login token. If this doesn't
  69. work, redirect to the password login page with an error message
  70. * Validate the confirmation code provided by the user. If it's not
  71. valid, redirect to the previous login step with an error message
  72. * Generate a long-term MFA login token for the user and log the
  73. user in using the token
  74. * Build a trusted device cookie and add it to the response if the
  75. trusted device option was checked
  76. * Redirect to the provided URL or to '/web' if one was not given
  77. """
  78. # sudo() is required because there is no request.env.uid (likely since
  79. # there is no user logged in at the start of the request)
  80. user_model_sudo = request.env['res.users'].sudo()
  81. device_model_sudo = user_model_sudo.env['res.users.device']
  82. config_model_sudo = user_model_sudo.env['ir.config_parameter']
  83. token = request.params.get('mfa_login_token')
  84. try:
  85. user = user_model_sudo.user_from_mfa_login_token(token)
  86. except (MfaTokenInvalidError, MfaTokenExpiredError) as exception:
  87. return http.local_redirect(
  88. '/web/login',
  89. query={
  90. 'redirect': request.params.get('redirect'),
  91. 'error': exception.message,
  92. },
  93. keep_hash=True,
  94. )
  95. confirmation_code = request.params.get('confirmation_code')
  96. if not user.validate_mfa_confirmation_code(confirmation_code):
  97. return http.local_redirect(
  98. '/auth_totp/login',
  99. query={
  100. 'redirect': request.params.get('redirect'),
  101. 'error': _(
  102. 'Your confirmation code is not correct. Please try'
  103. ' again.'
  104. ),
  105. 'mfa_login_token': token,
  106. },
  107. keep_hash=True,
  108. )
  109. # These context managers trigger a safe commit, which persists the
  110. # changes right away and is needed for the auth call
  111. with Environment.manage():
  112. with registry(request.db).cursor() as temp_cr:
  113. temp_env = Environment(temp_cr, SUPERUSER_ID, request.context)
  114. temp_user = temp_env['res.users'].browse(user.id)
  115. temp_user.generate_mfa_login_token(60 * 24 * 30)
  116. token = temp_user.mfa_login_token
  117. request.session.authenticate(request.db, user.login, token, user.id)
  118. request.params['login_success'] = True
  119. redirect = request.params.get('redirect')
  120. if not redirect:
  121. redirect = '/web'
  122. response = http.redirect_with_hash(redirect)
  123. if not isinstance(response, WerkzeugResponse):
  124. response = Response(response)
  125. if request.params.get('remember_device'):
  126. device = device_model_sudo.create({'user_id': user.id})
  127. secret = config_model_sudo.get_param('database.secret')
  128. device_cookie = JsonSecureCookie({'device_id': device.id}, secret)
  129. cookie_lifetime = timedelta(days=30)
  130. cookie_exp = datetime.utcnow() + cookie_lifetime
  131. device_cookie = device_cookie.serialize(cookie_exp)
  132. cookie_key = 'trusted_devices_%d' % user.id
  133. sec_config = config_model_sudo.get_param('auth_totp.secure_cookie')
  134. security_flag = sec_config != '0'
  135. response.set_cookie(
  136. cookie_key,
  137. device_cookie,
  138. max_age=cookie_lifetime.total_seconds(),
  139. expires=cookie_exp,
  140. httponly=True,
  141. secure=security_flag,
  142. )
  143. return response