Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DRAFT][15.0][IMP] auth_password_pwned: check passwords against haveibeenpwned.com #645

Draft
wants to merge 2 commits into
base: 15.0
Choose a base branch
from
Draft
Show file tree
Hide file tree
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
100 changes: 100 additions & 0 deletions auth_password_pwned/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
====================
Password Pwned Check
====================

..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:8ef5c3ff5b64085cdfcd667aed1caed570703d9eb90128e264c47f6967a2de9d
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |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-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--auth-lightgray.png?logo=github
:target: https://github.com/OCA/server-auth/tree/15.0/auth_password_pwned
: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-15-0/server-auth-15-0-auth_password_pwned
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/server-auth&target_branch=15.0
:alt: Try me on Runboat

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

This module enforces passwords to be changed once the have appeared in a data breach.

It uses https://haveibeenpwned.com/API/v3#SearchingPwnedPasswordsByRange to check if the password has appeared in any
data breaches. A great resource provided by Troy Hunt https://haveibeenpwned.com/About .

**Table of contents**

.. contents::
:local:

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

ir.config_parameter options
~~~~~~~~~~~~~~~~~~~~~~~~~~~

The following config parameters change the behaviour of this addon.

``auth_password_pwned.range_url`` *string* (Default: https://api.pwnedpasswords.com/range/)

Change the url the plugins checks hashes against. Needs to behave like described in
https://haveibeenpwned.com/API/v3#SearchingPwnedPasswordsByRange . This is intended to be used for a company mirror
of the API.

Usage
=====

Install the plugin to force the users to change their password once it is considered to be publicly known.

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 to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/server-auth/issues/new?body=module:%20auth_password_pwned%0Aversion:%2015.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
~~~~~~~

* WT-IO-IT GmbH

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


* `WT-IO-IT GmbH <https://www.wt-io-it.at>`_:
* Andreas Perhab <[email protected]>

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/15.0/auth_password_pwned>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
2 changes: 2 additions & 0 deletions auth_password_pwned/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import controllers
from . import models
20 changes: 20 additions & 0 deletions auth_password_pwned/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
{
"name": "Password Pwned Check",
"summary": "Prevent using pwned passwords.",
"version": "15.0.1.0.0",
"author": "WT-IO-IT GmbH, Odoo Community Association (OCA)",
"category": "Base",
"depends": [],
"website": "https://github.com/OCA/server-auth",
"external_dependencies": {},
"license": "AGPL-3",
"data": [],
"assets": {
'web.assets_tests': [
'auth_password_pwned/static/tests/tours/**/*',
],
},
"demo": [],
"installable": True,
}
3 changes: 3 additions & 0 deletions auth_password_pwned/controllers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import main
# TODO only in tests
from . import test_controllers
85 changes: 85 additions & 0 deletions auth_password_pwned/controllers/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

import logging

from odoo import _, http
from odoo.http import request

from odoo.addons.web.controllers.main import Home

_logger = logging.getLogger(__name__)


class AuthPasswordPwnedHome(Home):
def _auth_signup_is_installed(self):
return bool(
request.env["ir.module.module"]
.sudo()
.search(
[("name", "=", "auth_signup"), ("state", "in", ["installed"])], limit=1
)
)

def _reset_password_enabled(self):
return (
request.env["ir.config_parameter"]
.sudo()
.get_param("auth_signup.reset_password")
== "True"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems aberrant to check for a string value to exactly equal True, but that does seem to match what the auth_signup module does.

)

@http.route()
def web_login(self, *args, **kw):
pwned = False
reset_pw_after_validation = False
if "password" in kw and request.env.user._passwordhasbeenpwned(kw["password"]):
if self._auth_signup_is_installed():
if self._reset_password_enabled():
# prevent login with a pwned password and force user to reset it
kw["password"] = ""
request.params["password"] = ""
pwned = _(
"This password is known by third parties please reset it and"
" use a different password."
)
else:
# prepare to hint the user to their email
# reset the password after it has been validated
reset_pw_after_validation = True
pwned = _(
"This password is known by third parties an email has been"
" sent with instructions how to reset it."
)

else:
# display a login message and tell the user they should contact the admin
# to start a safe password change procedure
kw["password"] = ""
request.params["password"] = ""
pwned = _(
"This password is known by third parties please contact an"
" administrator how to get a new one."
)
response = super().web_login(*args, **kw)
if reset_pw_after_validation and request.params.get("login_success"):
# do not allow user to continue and send them a reset password email
request.params["login_success"] = False
request.session.logout(keep_db=True)
try:
request.env["res.users"].sudo().reset_password(kw["login"])
except Exception as e:
# Log the exception and continue to tell the "user" an email has been sent.
# reset_password only throws an exception if the login is not correct
# / not active so this is most likely someone guessing usernames.
_logger.error(
_("Could not reset password for {login}: {exception}").format(
login=kw["login"], exception=e
)
)
# make the response render the login with our error message
kw["password"] = ""
request.params["password"] = ""
response = super().web_login(*args, **kw)
if pwned:
response.qcontext["error"] = pwned
return response
12 changes: 12 additions & 0 deletions auth_password_pwned/controllers/test_controllers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from odoo.http import Controller, route, Response

KNOWN_HASHES=[]


class TestRangeController(Controller):

@route("/auth_password_pwned/range/<string:range>", type="http", auth="none")
def test_pwned_range(self, range):
return Response("\n".join([
f"{k[len(range):]}:1" for k in KNOWN_HASHES if k.startswith(range)
]), content_type="text/plain", status=200)
1 change: 1 addition & 0 deletions auth_password_pwned/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import res_users
63 changes: 63 additions & 0 deletions auth_password_pwned/models/res_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import hashlib
import logging

import requests

from odoo import _, models
from odoo.exceptions import UserError

_logger = logging.getLogger(__name__)


class ResUsers(models.Model):
_inherit = "res.users"

def _set_password(self):
self._passwordshavebeenpwned(self.mapped("password"))

return super()._set_password()

def _passwordshavebeenpwned(self, passwords):
for password in passwords:
if self._passwordhasbeenpwned(password):
raise UserError(
_("Password is already pwned and can no longer be used.")
)

def _passwordhasbeenpwned(self, password):
params = self.env["ir.config_parameter"].sudo()
api_url = params.get_param(
"auth_password_pwned.range_url",
default="https://api.pwnedpasswords.com/range/",
)
if api_url[-1] == "/":
api_url = api_url[:-1]

password_hash = hashlib.sha1(password.encode("utf-8")).hexdigest().upper()
try:
r = requests.get(
"{api_url}/{hash}".format(api_url=api_url, hash=password_hash[:5]),
headers={
"User-Agent": "Odoo OCA auth_password_pwned"
" https://github.com/OCA/server-auth",
},
)
r.raise_for_status()
response = r.text
return password_hash[5:] in response
except (
requests.exceptions.HTTPError,
requests.exceptions.RequestException,
) as error:
if self.env.user.has_group("base.group_system"):
# for admins display a message for them being able to fix the issue
raise UserError(
_(f"{api_url} cannot be reached: {error}").format(api_url, error)
) from error
else:
# for other users log a warning
_logger.warning(
_(f"{api_url} cannot be reached: {error}").format(api_url, error)
)
# and let them log into the system (if they have the correct password)
return False
10 changes: 10 additions & 0 deletions auth_password_pwned/readme/CONFIGURE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
ir.config_parameter options
~~~~~~~~~~~~~~~~~~~~~~~~~~~

The following config parameters change the behaviour of this addon.

``auth_password_pwned.range_url`` *string* (Default: https://api.pwnedpasswords.com/range/)

Change the url the plugins checks hashes against. Needs to behave like described in
https://haveibeenpwned.com/API/v3#SearchingPwnedPasswordsByRange . This is intended to be used for a company mirror
of the API.
3 changes: 3 additions & 0 deletions auth_password_pwned/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

* `WT-IO-IT GmbH <https://www.wt-io-it.at>`_:
* Andreas Perhab <[email protected]>
4 changes: 4 additions & 0 deletions auth_password_pwned/readme/DESCRIPTION.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
This module enforces passwords to be changed once the have appeared in a data breach.

It uses https://haveibeenpwned.com/API/v3#SearchingPwnedPasswordsByRange to check if the password has appeared in any
data breaches. A great resource provided by Troy Hunt https://haveibeenpwned.com/About .
1 change: 1 addition & 0 deletions auth_password_pwned/readme/USAGE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Install the plugin to force the users to change their password once it is considered to be publicly known.
Loading
Loading