Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
9a3aab5
auth_totp: MFA Support * Add relevant fields and methods to the res.u…
obulkin Dec 2, 2016
207fb13
auth_totp: Upgrade to v10 * Rename manifest * Change openerp referenc…
lasley Jan 18, 2017
532f8e8
auth_totp: Permissions fix and other tweaks * Slightly reword README …
obulkin May 17, 2017
de16962
auth_totp: Admin support * Add MFA fields to normal res.users form vi…
obulkin May 23, 2017
9b8d9cc
auth_totp: User deletion * Add ondelete='cascade' to the res.users.au…
obulkin Jul 5, 2017
e44454f
auth_totp: Website compatibility * Add website compatibility by modif…
obulkin Jul 12, 2017
23f37c6
auth_totp: Firefox support * Fix Firefox MFA login error by adding lo…
obulkin Aug 16, 2017
c425df0
OCA Transbot updated translations from Transifex
oca-transbot Dec 2, 2017
236528f
Ensure no special chars are passed to totp.provisioning_uri
grindtildeath Dec 22, 2017
8d8cb14
auth_totp: Various issues * Restructure controller and res.users logi…
obulkin Nov 17, 2017
cfeb08d
PR commit
obulkin Dec 14, 2017
7476841
OCA Transbot updated translations from Transifex
oca-transbot Jan 16, 2018
2656c58
auth_totp: RPC access * Add res.users logic to prevent RPC access for…
obulkin Jan 17, 2018
cb858b5
OCA Transbot updated translations from Transifex
oca-transbot Mar 3, 2018
a6c06b7
auth_totp: Migration to 11.0
MiquelRForgeFlow May 9, 2018
b41f13d
Add 🐒 patches
MiquelRForgeFlow May 10, 2018
cccd716
comments
MiquelRForgeFlow May 15, 2018
07b8ce7
Update auth_totp.pot
oca-travis Dec 12, 2018
405e736
Resolve merge conflict in manifest
fkantelberg Jan 30, 2019
8c1b99b
Update auth_totp.pot
oca-travis Mar 14, 2019
4b89ee3
Update translation files
oca-transbot Mar 17, 2019
31c11a9
Migrate to 12.0
fkantelberg Jun 22, 2019
6058b20
Adjust readme to use folder
fkantelberg Jun 23, 2019
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
119 changes: 119 additions & 0 deletions auth_totp/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
===========
MFA Support
===========

.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--auth-lightgray.png?logo=github
:target: https://github.com/OCA/server-auth/tree/12.0/auth_totp
:alt: OCA/server-auth
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/server-auth-12-0/server-auth-12-0-auth_totp
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png
:target: https://runbot.odoo-community.org/runbot/251/12.0
:alt: Try me on Runbot

|badge1| |badge2| |badge3| |badge4| |badge5|

This module adds support for MFA using TOTP (time-based, one-time passwords).
It allows users to enable/disable MFA and manage authentication apps/devices
via the "Change My Preferences" view and an associated wizard.

After logging in normally, users with MFA enabled are taken to a second screen
where they have to enter a password generated by one of their authentication
apps and are presented with the option to remember the current device. This
creates a secure, HTTP-only cookie that allows subsequent logins to bypass the
MFA step.

**Table of contents**

.. contents::
:local:

Installation
============

1. Install the PyOTP library using pip: ``pip install pyotp``
2. Follow the standard module install process

Configuration
=============

By default, the trusted device cookies introduced by this module have a
``Secure`` flag. This decreases the likelihood of cookie theft via
eavesdropping but may result in cookies not being set by certain browsers
unless your Odoo instance uses HTTPS. If necessary, you can disable this flag
by going to ``Settings > Parameters > System Parameters`` and changing the
``auth_totp.secure_cookie`` key to ``0``.

Usage
=====

If necessary, a user's trusted devices can be revoked by disabling and
re-enabling MFA for that user.

Known issues / Roadmap
======================

* External calls to the Odoo XML-RPC API are blocked for users who enable MFA
since there is currently no way to perform MFA authentication as part of this
process. However, due to the way that Odoo handles authentication caching,
multi-threaded or multi-process servers will need to be restarted before the
block can take effect for users who have just enabled MFA.
* Make the lifetime of the trusted device cookie configurable rather than fixed
at 30 days
* Add device fingerprinting to the trusted device cookie
* Add company-level settings for forcing all users to enable MFA and disabling
the trusted device option
* Monkey patch 1 is not needed anymore in Werkzeug==0.13 or upper
* Monkey patch 2 will work until werkzeug.contrib gets removed.

Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/OCA/server-auth/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
`feedback <https://github.com/OCA/server-auth/issues/new?body=module:%20auth_totp%0Aversion:%2012.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

Do not contact contributors directly about support or help with technical issues.

Credits
=======

Authors
~~~~~~~

* LasLabs

Contributors
~~~~~~~~~~~~

* Oleg Bulkin <obulkin@laslabs.com>

Maintainers
~~~~~~~~~~~

This module is maintained by the OCA.

.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org

OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.

This module is part of the `OCA/server-auth <https://github.com/OCA/server-auth/tree/12.0/auth_totp>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
6 changes: 6 additions & 0 deletions auth_totp/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).

from . import controllers
from . import exceptions
from . import models
from . import wizards
28 changes: 28 additions & 0 deletions auth_totp/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Copyright 2016-2017 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).

{
'name': 'MFA Support',
'summary': 'Allows users to enable MFA and add optional trusted devices',
'version': '12.0.1.0.0',
'category': 'Tools',
'website': 'https://github.com/OCA/server-auth',
'author': 'LasLabs, Odoo Community Association (OCA)',
'license': 'LGPL-3',
'application': False,
'installable': True,
'external_dependencies': {
'python': ['pyotp'],
},
'depends': [
'web',
],
'data': [
'data/ir_config_parameter.xml',
'security/ir.model.access.csv',
'security/res_users_authenticator_security.xml',
'wizards/res_users_authenticator_create.xml',
'views/auth_totp.xml',
'views/res_users.xml',
],
}
3 changes: 3 additions & 0 deletions auth_totp/controllers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).

from . import main
167 changes: 167 additions & 0 deletions auth_totp/controllers/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# Copyright 2016-2017 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).

from datetime import datetime, timedelta
import base64
import json
from werkzeug.contrib.securecookie import SecureCookie, UnquoteError
from werkzeug.contrib.sessions import ModificationTrackingDict
from werkzeug._compat import to_bytes
from werkzeug.wrappers import Response as WerkzeugResponse
from odoo import _, http
from odoo.http import Response, request
from odoo.addons.web.controllers.main import Home


class JsonSecureCookie(SecureCookie):
serialization_method = json

# Monkey patch 1:
# see: https://github.com/pallets/werkzeug/commit/
# 576b75d85e97fc23441e766a9714edc6993bde25

def __init__(self, data=None, secret_key=None, new=True):
ModificationTrackingDict.__init__(self, data or ())
# explicitly convert it into a bytestring because python 2.6
# no longer performs an implicit string conversion on hmac
if secret_key is not None:
secret_key = to_bytes(secret_key, 'utf-8')
self.secret_key = secret_key
self.new = new

# Monkey patch 2:
# see: https://github.com/pallets/werkzeug/issues/953

@classmethod
def quote(cls, value):
if cls.serialization_method is not None:
value = cls.serialization_method.dumps(value)
value = value.encode('utf-8')
if cls.quote_base64:
value = b''.join(base64.b64encode(value).splitlines()).strip()
return value

@classmethod
def unquote(cls, value):
try:
if cls.quote_base64:
value = base64.b64decode(value)
value = str(value, 'utf-8')
if cls.serialization_method is not None:
value = cls.serialization_method.loads(value)
return value
except Exception:
raise UnquoteError()


class AuthTotp(Home):
@http.route()
def web_login(self, *args, **kwargs):
response = super(AuthTotp, self).web_login(*args, **kwargs)

if request.session.get('mfa_login_needed'):
request.session.update({
'mfa_login_needed': False,
'login': kwargs.get('login', None),
'password': kwargs.get('password', None),
})
return http.local_redirect(
'/auth_totp/login',
query={'redirect': request.params.get('redirect')},
keep_hash=True,
)

return response

@http.route(
'/auth_totp/login',
type='http',
auth='public',
methods=['GET'],
website=True,
)
def mfa_login_get(self, *args, **kwargs):
return request.render('auth_totp.mfa_login', qcontext=request.params)

@http.route('/auth_totp/login', type='http', auth='none', methods=['POST'])
def mfa_login_post(self, *args, **kwargs):
"""Process MFA login attempt.

Overview:
* Identify current user based on login in session. If this doesn't
work, redirect to the password login page with an error message.
* Validate the confirmation code provided by the user. If it's not
valid, redirect to the previous login step with an error message.
* Update the session to indicate that the MFA login process for
this user is complete and attempt password authentication again.
* Build a trusted device cookie and add it to the response if the
trusted device option was checked.
* Redirect to the provided URL or to '/web' if one was not given.
"""

# sudo() is required because there is no request.env.uid (likely since
# there is no user logged in at the start of the request)
user_model_sudo = request.env['res.users'].sudo()
config_model_sudo = user_model_sudo.env['ir.config_parameter'].sudo()

user_login = request.session.get('login')
user = user_model_sudo.search([('login', '=', user_login)])
if not user:
return http.local_redirect(
'/web/login',
query={
'redirect': request.params.get('redirect'),
'error': _(
'You must log in with a password before starting the'
' MFA login process.'
),
},
keep_hash=True,
)

confirmation_code = request.params.get('confirmation_code')
if not user.validate_mfa_confirmation_code(confirmation_code):
return http.local_redirect(
'/auth_totp/login',
query={
'redirect': request.params.get('redirect'),
'error': _(
'Your confirmation code is not correct. Please try'
' again.'
),
},
keep_hash=True,
)
request.session['mfa_login_active'] = user.id

user_pass = request.session.get('password')
uid = request.session.authenticate(request.db, user.login, user_pass)
if uid:
request.params['login_success'] = True

redirect = request.params.get('redirect')
if not redirect:
redirect = '/web'
response = http.redirect_with_hash(redirect)
if not isinstance(response, WerkzeugResponse):
response = Response(response)

if request.params.get('remember_device'):
secret = user.trusted_device_cookie_key
device_cookie = JsonSecureCookie({'user_id': user.id}, secret)
cookie_lifetime = timedelta(days=30)
cookie_exp = datetime.utcnow() + cookie_lifetime
device_cookie = device_cookie.serialize(cookie_exp)
cookie_key = 'trusted_devices_%d' % user.id
sec_config = config_model_sudo.get_param('auth_totp.secure_cookie')
security_flag = sec_config != '0'
response.set_cookie(
cookie_key,
device_cookie,
max_age=cookie_lifetime.total_seconds(),
expires=cookie_exp,
httponly=True,
secure=security_flag,
)

return response
13 changes: 13 additions & 0 deletions auth_totp/data/ir_config_parameter.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>

<!--
Copyright 2016-2017 LasLabs Inc.
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
-->

<odoo noupdate="1">
<record id="cookie_security" model="ir.config_parameter">
<field name="key">auth_totp.secure_cookie</field>
<field name="value" eval="1"/>
</record>
</odoo>
8 changes: 8 additions & 0 deletions auth_totp/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Copyright 2016-2017 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).

from odoo.exceptions import AccessDenied


class MfaLoginNeeded(AccessDenied):
pass
Loading