├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── Makefile ├── NOTICE ├── README.md ├── README.rst ├── __init__.py ├── django_pwnedpasswords_validator ├── __init__.py ├── metadata.py └── validation.py ├── example ├── README.md ├── django_pwnedpasswords_validator ├── manage.py ├── templates │ ├── register.html │ └── success.html └── testapp │ ├── __init__.py │ ├── forms.py │ ├── settings.py │ ├── templates │ └── register.html │ ├── urls.py │ ├── views.py │ └── wsgi.py ├── meta ├── header.sketch ├── repo-banner-2.png ├── repo-banner-bottom.png ├── repo-banner-small.png └── repo-banner.png ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .DS_Store 3 | *.egg-info/ 4 | build/ 5 | dist/ 6 | *.un~ 7 | .tox 8 | .eggs/ 9 | *.sqlite3 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.5" 5 | - "3.6" 6 | - "pypy" 7 | install: "pip install -r requirements.txt" 8 | script: "python setup.py test" 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | 3 | Apache License 4 | Version 2.0, January 2004 5 | http://www.apache.org/licenses/ 6 | 7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, 12 | and distribution as defined by Sections 1 through 9 of this document. 13 | 14 | "Licensor" shall mean the copyright owner or entity authorized by 15 | the copyright owner that is granting the License. 16 | 17 | "Legal Entity" shall mean the union of the acting entity and all 18 | other entities that control, are controlled by, or are under common 19 | control with that entity. For the purposes of this definition, 20 | "control" means (i) the power, direct or indirect, to cause the 21 | direction or management of such entity, whether by contract or 22 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 23 | outstanding shares, or (iii) beneficial ownership of such entity. 24 | 25 | "You" (or "Your") shall mean an individual or Legal Entity 26 | exercising permissions granted by this License. 27 | 28 | "Source" form shall mean the preferred form for making modifications, 29 | including but not limited to software source code, documentation 30 | source, and configuration files. 31 | 32 | "Object" form shall mean any form resulting from mechanical 33 | transformation or translation of a Source form, including but 34 | not limited to compiled object code, generated documentation, 35 | and conversions to other media types. 36 | 37 | "Work" shall mean the work of authorship, whether in Source or 38 | Object form, made available under the License, as indicated by a 39 | copyright notice that is included in or attached to the work 40 | (an example is provided in the Appendix below). 41 | 42 | "Derivative Works" shall mean any work, whether in Source or Object 43 | form, that is based on (or derived from) the Work and for which the 44 | editorial revisions, annotations, elaborations, or other modifications 45 | represent, as a whole, an original work of authorship. For the purposes 46 | of this License, Derivative Works shall not include works that remain 47 | separable from, or merely link (or bind by name) to the interfaces of, 48 | the Work and Derivative Works thereof. 49 | 50 | "Contribution" shall mean any work of authorship, including 51 | the original version of the Work and any modifications or additions 52 | to that Work or Derivative Works thereof, that is intentionally 53 | submitted to Licensor for inclusion in the Work by the copyright owner 54 | or by an individual or Legal Entity authorized to submit on behalf of 55 | the copyright owner. For the purposes of this definition, "submitted" 56 | means any form of electronic, verbal, or written communication sent 57 | to the Licensor or its representatives, including but not limited to 58 | communication on electronic mailing lists, source code control systems, 59 | and issue tracking systems that are managed by, or on behalf of, the 60 | Licensor for the purpose of discussing and improving the Work, but 61 | excluding communication that is conspicuously marked or otherwise 62 | designated in writing by the copyright owner as "Not a Contribution." 63 | 64 | "Contributor" shall mean Licensor and any individual or Legal Entity 65 | on behalf of whom a Contribution has been received by Licensor and 66 | subsequently incorporated within the Work. 67 | 68 | 2. Grant of Copyright License. Subject to the terms and conditions of 69 | this License, each Contributor hereby grants to You a perpetual, 70 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 71 | copyright license to reproduce, prepare Derivative Works of, 72 | publicly display, publicly perform, sublicense, and distribute the 73 | Work and such Derivative Works in Source or Object form. 74 | 75 | 3. Grant of Patent License. Subject to the terms and conditions of 76 | this License, each Contributor hereby grants to You a perpetual, 77 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 78 | (except as stated in this section) patent license to make, have made, 79 | use, offer to sell, sell, import, and otherwise transfer the Work, 80 | where such license applies only to those patent claims licensable 81 | by such Contributor that are necessarily infringed by their 82 | Contribution(s) alone or by combination of their Contribution(s) 83 | with the Work to which such Contribution(s) was submitted. If You 84 | institute patent litigation against any entity (including a 85 | cross-claim or counterclaim in a lawsuit) alleging that the Work 86 | or a Contribution incorporated within the Work constitutes direct 87 | or contributory patent infringement, then any patent licenses 88 | granted to You under this License for that Work shall terminate 89 | as of the date such litigation is filed. 90 | 91 | 4. Redistribution. You may reproduce and distribute copies of the 92 | Work or Derivative Works thereof in any medium, with or without 93 | modifications, and in Source or Object form, provided that You 94 | meet the following conditions: 95 | 96 | (a) You must give any other recipients of the Work or 97 | Derivative Works a copy of this License; and 98 | 99 | (b) You must cause any modified files to carry prominent notices 100 | stating that You changed the files; and 101 | 102 | (c) You must retain, in the Source form of any Derivative Works 103 | that You distribute, all copyright, patent, trademark, and 104 | attribution notices from the Source form of the Work, 105 | excluding those notices that do not pertain to any part of 106 | the Derivative Works; and 107 | 108 | (d) If the Work includes a "NOTICE" text file as part of its 109 | distribution, then any Derivative Works that You distribute must 110 | include a readable copy of the attribution notices contained 111 | within such NOTICE file, excluding those notices that do not 112 | pertain to any part of the Derivative Works, in at least one 113 | of the following places: within a NOTICE text file distributed 114 | as part of the Derivative Works; within the Source form or 115 | documentation, if provided along with the Derivative Works; or, 116 | within a display generated by the Derivative Works, if and 117 | wherever such third-party notices normally appear. The contents 118 | of the NOTICE file are for informational purposes only and 119 | do not modify the License. You may add Your own attribution 120 | notices within Derivative Works that You distribute, alongside 121 | or as an addendum to the NOTICE text from the Work, provided 122 | that such additional attribution notices cannot be construed 123 | as modifying the License. 124 | 125 | You may add Your own copyright statement to Your modifications and 126 | may provide additional or different license terms and conditions 127 | for use, reproduction, or distribution of Your modifications, or 128 | for any such Derivative Works as a whole, provided Your use, 129 | reproduction, and distribution of the Work otherwise complies with 130 | the conditions stated in this License. 131 | 132 | 5. Submission of Contributions. Unless You explicitly state otherwise, 133 | any Contribution intentionally submitted for inclusion in the Work 134 | by You to the Licensor shall be under the terms and conditions of 135 | this License, without any additional terms or conditions. 136 | Notwithstanding the above, nothing herein shall supersede or modify 137 | the terms of any separate license agreement you may have executed 138 | with Licensor regarding such Contributions. 139 | 140 | 6. Trademarks. This License does not grant permission to use the trade 141 | names, trademarks, service marks, or product names of the Licensor, 142 | except as required for reasonable and customary use in describing the 143 | origin of the Work and reproducing the content of the NOTICE file. 144 | 145 | 7. Disclaimer of Warranty. Unless required by applicable law or 146 | agreed to in writing, Licensor provides the Work (and each 147 | Contributor provides its Contributions) on an "AS IS" BASIS, 148 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 149 | implied, including, without limitation, any warranties or conditions 150 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 151 | PARTICULAR PURPOSE. You are solely responsible for determining the 152 | appropriateness of using or redistributing the Work and assume any 153 | risks associated with Your exercise of permissions under this License. 154 | 155 | 8. Limitation of Liability. In no event and under no legal theory, 156 | whether in tort (including negligence), contract, or otherwise, 157 | unless required by applicable law (such as deliberate and grossly 158 | negligent acts) or agreed to in writing, shall any Contributor be 159 | liable to You for damages, including any direct, indirect, special, 160 | incidental, or consequential damages of any character arising as a 161 | result of this License or out of the use or inability to use the 162 | Work (including but not limited to damages for loss of goodwill, 163 | work stoppage, computer failure or malfunction, or any and all 164 | other commercial damages or losses), even if such Contributor 165 | has been advised of the possibility of such damages. 166 | 167 | 9. Accepting Warranty or Additional Liability. While redistributing 168 | the Work or Derivative Works thereof, You may choose to offer, 169 | and charge a fee for, acceptance of support, warranty, indemnity, 170 | or other liability obligations and/or rights consistent with this 171 | License. However, in accepting such obligations, You may act only 172 | on Your own behalf and on Your sole responsibility, not on behalf 173 | of any other Contributor, and only if You agree to indemnify, 174 | defend, and hold each Contributor harmless for any liability 175 | incurred by, or claims asserted against, such Contributor by reason 176 | of your accepting any such warranty or additional liability. 177 | 178 | END OF TERMS AND CONDITIONS 179 | 180 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include README.md 3 | include LICENSE 4 | include NOTICE 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2014-2018 Lionheart Software LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # Usage: make VERSION=0.1.2 16 | 17 | METADATA_FILE := $(shell find . -name "metadata.py" -depth 2) 18 | 19 | all: clean test publish 20 | 21 | clean: 22 | rm -rf dist/ 23 | 24 | test: 25 | python setup.py test 26 | python3 setup.py test 27 | 28 | update_readme: 29 | pandoc --from=markdown --to=rst --output=README.rst README.md 30 | -git reset 31 | -git add README.rst 32 | -git commit -m "update README.rst from README.md" 33 | 34 | update_version: 35 | sed -i "" "s/\(__version__[ ]*=\).*/\1 \"$(VERSION)\"/g" $(METADATA_FILE) 36 | git add . 37 | # - ignores errors in this command 38 | -git commit -m "bump version to $(VERSION)" 39 | # Delete tag if already exists 40 | -git tag -d $(VERSION) 41 | -git push origin master :$(VERSION) 42 | git tag $(VERSION) 43 | git push origin master 44 | git push --tags 45 | 46 | publish: clean update_readme update_version 47 | python setup.py bdist_wheel --universal 48 | python3 setup.py bdist_wheel --universal 49 | gpg --detach-sign -a dist/*.whl 50 | twine upload dist/* 51 | 52 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | django-pwnedpasswords-validator 2 | Copyright 2018-2018 Lionheart Software LLC. 3 | 4 | This product includes software developed by Lionheart Software (https://lionheartsw.com/). 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](meta/repo-banner-2.png) 2 | [![](meta/repo-banner-bottom.png)][lionheart-url] 3 | 4 | [![Version][version-badge]][pypi-url] 5 | [![Python Versions][versions-badge]][pypi-url] 6 | 7 | `django-pwnedpasswords-validator` is a Django password validator that checks if a user-provided password exists in a data breach using the [Pwned Passwords v2 API](https://haveibeenpwned.com/API/v2#PwnedPasswords). All provided password data is [k-anonymized][k-anonymous-url] before being sent to the API, so plaintext passwords never leave your server. 8 | 9 | From https://haveibeenpwned.com/API/v2#PwnedPasswords: 10 | 11 | > Pwned Passwords are more than half a billion passwords which have previously been exposed in data breaches. The service is detailed in the [launch blog post](https://www.troyhunt.com/introducing-306-million-freely-downloadable-pwned-passwords/) then [further expanded on with the release of version 2](https://www.troyhunt.com/ive-just-launched-pwned-passwords-version-2). The entire data set is [both downloadable and searchable online via the Pwned Passwords page](https://haveibeenpwned.com/Passwords). 12 | 13 | ## Installation 14 | 15 | django-pwnedpasswords-validator is available for download through [PyPi][pypi-url]. You can install it right away using pip. 16 | 17 | ```bash 18 | pip install django-pwnedpasswords-validator 19 | ``` 20 | 21 | Then, add django-pwnedpasswords-validator to your `INSTALLED_APPS`: 22 | 23 | ```python 24 | INSTALLED_APPS = ( 25 | ... 26 | 'django_pwnedpasswords_validator' 27 | ) 28 | ``` 29 | 30 | Finally, add django-pwnedpasswords-validator to `AUTH_PASSWORD_VALIDATORS`: 31 | 32 | ```python 33 | AUTH_PASSWORD_VALIDATORS = [ 34 | ... 35 | { 36 | 'NAME': "django_pwnedpasswords_validator.validation.PwnedPasswordValidator" 37 | } 38 | ] 39 | ``` 40 | 41 | If you'd like to customize the error message (the default is *"This password has previously appeared in a data breach and should not be used."*), you can pass in an alternate in the `OPTIONS` parameter for the validator. 42 | 43 | ```python 44 | AUTH_PASSWORD_VALIDATORS = [ 45 | ... 46 | { 47 | 'NAME': "django_pwnedpasswords_validator.validation.PwnedPasswordValidator", 48 | 'OPTIONS': { 49 | 'error_text': "Your password was found in a data breach.", 50 | } 51 | } 52 | ] 53 | ``` 54 | 55 | #### Security Note 56 | 57 | No plaintext passwords ever leave your server using django-pwnedpasswords-validator. 58 | 59 | How does that work? Well, the Pwned Passwords v2 API has a pretty cool [k-anonymity][k-anonymous-url] implementation. 60 | 61 | From https://blog.cloudflare.com/validating-leaked-passwords-with-k-anonymity/: 62 | 63 | > Formally, a data set can be said to hold the property of k-anonymity, if for every record in a released table, there are k − 1 other records identical to it. 64 | 65 | This allows us to only provide the first 5 characters of the SHA-1 hash of the password in question. The API then responds with a list of SHA-1 hash suffixes with that prefix. On average, that list contains 478 results. 66 | 67 | People smarter than I am have used [math](https://blog.cloudflare.com/validating-leaked-passwords-with-k-anonymity/) to prove that 5-character prefixes are sufficient to maintain k-anonymity for this database. 68 | 69 | In short: your plaintext passwords are protected if you use this library. You won't leak any enough data to identity which passwords you're searching for. 70 | 71 | ## Thanks 72 | 73 | Special thanks to [Troy Hunt](https://www.troyhunt.com) for collecting this data and providing this service. 74 | 75 | ## Authors 76 | 77 | [Dan Loewenherz](https://github.com/dlo) 78 | 79 | ## See also 80 | 81 | [pwnedpasswords](https://github.com/lionheart/pwnedpasswords), a command-line utility and Python library for the Pwned Passwords v2 API. 82 | 83 | ## License 84 | 85 | Apache License, Version 2.0. See [LICENSE](LICENSE) for details. 86 | 87 | [ci-badge]: https://img.shields.io/travis/lionheart/django-pwnedpasswords-validator.svg?style=flat 88 | [version-badge]: https://img.shields.io/pypi/v/django-pwnedpasswords-validator.svg?style=flat 89 | [versions-badge]: https://img.shields.io/pypi/pyversions/django-pwnedpasswords-validator.svg?style=flat 90 | 91 | [travis-repo-url]: https://travis-ci.org/lionheart/django-pwnedpasswords-validator 92 | [k-anonymous-url]: https://en.wikipedia.org/wiki/K-anonymity 93 | [semver-url]: http://www.semver.org 94 | [pypi-url]: https://pypi.python.org/pypi/django-pwnedpasswords-validator 95 | [lionheart-url]: https://lionheartsw.com/ 96 | 97 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | |image0| |image1| 2 | 3 | |Version| |Python Versions| 4 | 5 | ``django-pwnedpasswords-validator`` is a Django password validator that 6 | checks if a user-provided password exists in a data breach using the 7 | `Pwned Passwords v2 8 | API `__. All provided 9 | password data is 10 | `k-anonymized `__ before 11 | being sent to the API, so plaintext passwords never leave your server. 12 | 13 | From https://haveibeenpwned.com/API/v2#PwnedPasswords: 14 | 15 | Pwned Passwords are more than half a billion passwords which have 16 | previously been exposed in data breaches. The service is detailed in 17 | the `launch blog 18 | post `__ 19 | then `further expanded on with the release of version 20 | 2 `__. 21 | The entire data set is `both downloadable and searchable online via 22 | the Pwned Passwords page `__. 23 | 24 | Installation 25 | ------------ 26 | 27 | django-pwnedpasswords-validator is available for download through 28 | `PyPi `__. 29 | You can install it right away using pip. 30 | 31 | .. code:: bash 32 | 33 | pip install django-pwnedpasswords-validator 34 | 35 | Then, add django-pwnedpasswords-validator to your ``INSTALLED_APPS``: 36 | 37 | .. code:: python 38 | 39 | INSTALLED_APPS = ( 40 | ... 41 | 'django_pwnedpasswords_validator' 42 | ) 43 | 44 | Finally, add django-pwnedpasswords-validator to 45 | ``AUTH_PASSWORD_VALIDATORS``: 46 | 47 | .. code:: python 48 | 49 | AUTH_PASSWORD_VALIDATORS = [ 50 | ... 51 | { 52 | 'NAME': "django_pwnedpasswords_validator.validation.PwnedPasswordValidator" 53 | } 54 | ] 55 | 56 | If you’d like to customize the error message (the default is *“This 57 | password has previously appeared in a data breach and should not be 58 | used.”*), you can pass in an alternate in the ``OPTIONS`` parameter for 59 | the validator. 60 | 61 | .. code:: python 62 | 63 | AUTH_PASSWORD_VALIDATORS = [ 64 | ... 65 | { 66 | 'NAME': "django_pwnedpasswords_validator.validation.PwnedPasswordValidator", 67 | 'OPTIONS': { 68 | 'error_text': "Your password was found in a data breach.", 69 | } 70 | } 71 | ] 72 | 73 | Security Note 74 | ^^^^^^^^^^^^^ 75 | 76 | No plaintext passwords ever leave your server using 77 | django-pwnedpasswords-validator. 78 | 79 | How does that work? Well, the Pwned Passwords v2 API has a pretty cool 80 | `k-anonymity `__ 81 | implementation. 82 | 83 | From 84 | https://blog.cloudflare.com/validating-leaked-passwords-with-k-anonymity/: 85 | 86 | Formally, a data set can be said to hold the property of 87 | k-anonymity, if for every record in a released table, there are k − 88 | 1 other records identical to it. 89 | 90 | This allows us to only provide the first 5 characters of the SHA-1 hash 91 | of the password in question. The API then responds with a list of SHA-1 92 | hash suffixes with that prefix. On average, that list contains 478 93 | results. 94 | 95 | People smarter than I am have used 96 | `math `__ 97 | to prove that 5-character prefixes are sufficient to maintain 98 | k-anonymity for this database. 99 | 100 | In short: your plaintext passwords are protected if you use this 101 | library. You won’t leak any enough data to identity which passwords 102 | you’re searching for. 103 | 104 | Thanks 105 | ------ 106 | 107 | Special thanks to `Troy Hunt `__ for 108 | collecting this data and providing this service. 109 | 110 | Authors 111 | ------- 112 | 113 | `Dan Loewenherz `__ 114 | 115 | See also 116 | -------- 117 | 118 | `pwnedpasswords `__, a 119 | command-line utility and Python library for the Pwned Passwords v2 API. 120 | 121 | License 122 | ------- 123 | 124 | Apache License, Version 2.0. See `LICENSE `__ for details. 125 | 126 | .. |image0| image:: meta/repo-banner-2.png 127 | .. |image1| image:: meta/repo-banner-bottom.png 128 | :target: https://lionheartsw.com/ 129 | .. |Version| image:: https://img.shields.io/pypi/v/django-pwnedpasswords-validator.svg?style=flat 130 | :target: https://pypi.python.org/pypi/django-pwnedpasswords-validator 131 | .. |Python Versions| image:: https://img.shields.io/pypi/pyversions/django-pwnedpasswords-validator.svg?style=flat 132 | :target: https://pypi.python.org/pypi/django-pwnedpasswords-validator 133 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lionheart/django-pwnedpasswords-validator/ac1b95eced5abacc884af8a3745d6de0a0db703a/__init__.py -------------------------------------------------------------------------------- /django_pwnedpasswords_validator/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2018 Lionheart Software LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from .metadata import ( 18 | __author__, 19 | __copyright__, 20 | __email__, 21 | __license__, 22 | __maintainer__, 23 | __version__, 24 | ) 25 | 26 | from .validation import PwnedPasswordValidator 27 | 28 | __all__ = [ 29 | '__author__', 30 | '__copyright__', 31 | '__email__', 32 | '__license__', 33 | '__maintainer__', 34 | '__version__', 35 | 'PwnedPasswordValidator', 36 | ] 37 | 38 | -------------------------------------------------------------------------------- /django_pwnedpasswords_validator/metadata.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Lionheart Software LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | __version__ = "1.0.1" 16 | __author__ = "Dan Loewenherz" 17 | __copyright__ = "Copyright 2018, Lionheart Software LLC" 18 | __maintainer__ = "Dan Loewenherz" 19 | __email__ = "dan@lionheartsw.com" 20 | __license__ = "Apache 2.0" 21 | 22 | -------------------------------------------------------------------------------- /django_pwnedpasswords_validator/validation.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2018 Lionheart Software LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from django.core.exceptions import ValidationError 18 | from django.utils.translation import gettext_lazy as _ 19 | 20 | import pwnedpasswords 21 | 22 | class PwnedPasswordValidator(object): 23 | ERROR_TEXT = _("This password has previously appeared in a data breach and should not be used.") 24 | 25 | def __init__(self, error_text=ERROR_TEXT, anonymous=True): 26 | self.anonymous = anonymous 27 | self.error_text = error_text 28 | 29 | def validate(self, password, user=None): 30 | count = pwnedpasswords.check(password, anonymous=self.anonymous, plain_text=True) 31 | if count > 0: 32 | raise ValidationError(_(self.error_text), code="password_is_pwned") 33 | 34 | def get_help_text(self): 35 | return _("Your password can't have appeared in a data breach.") 36 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | This is a working example Django application that shows how to use django-pwnedpasswords-validator in an existing project. 2 | -------------------------------------------------------------------------------- /example/django_pwnedpasswords_validator: -------------------------------------------------------------------------------- 1 | ../django-pwnedpasswords-validator/django_pwnedpasswords_validator -------------------------------------------------------------------------------- /example/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testapp.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError as exc: 10 | raise ImportError( 11 | "Couldn't import Django. Are you sure it's installed and " 12 | "available on your PYTHONPATH environment variable? Did you " 13 | "forget to activate a virtual environment?" 14 | ) from exc 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /example/templates/register.html: -------------------------------------------------------------------------------- 1 |
2 | {% csrf_token %} 3 | {{ form.as_p }} 4 | 5 |
6 | -------------------------------------------------------------------------------- /example/templates/success.html: -------------------------------------------------------------------------------- 1 | Yay 2 | -------------------------------------------------------------------------------- /example/testapp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lionheart/django-pwnedpasswords-validator/ac1b95eced5abacc884af8a3745d6de0a0db703a/example/testapp/__init__.py -------------------------------------------------------------------------------- /example/testapp/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib.auth.models import User 3 | from django.contrib.auth.password_validation import validate_password 4 | 5 | class UserForm(forms.ModelForm): 6 | class Meta(): 7 | model = User 8 | fields = ["email", "username", "password"] 9 | 10 | def clean_password(self): 11 | raw_password = self.cleaned_data['password'] 12 | print(raw_password) 13 | validate_password(raw_password) 14 | return raw_password 15 | -------------------------------------------------------------------------------- /example/testapp/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for testapp project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.0. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'p0l==a6%6jsy0n-83=y%tdk$a&*uoyxzr%%gtxi_b$lf^^(-bt' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'django_pwnedpasswords_validator' 41 | ] 42 | 43 | MIDDLEWARE = [ 44 | 'django.middleware.security.SecurityMiddleware', 45 | 'django.contrib.sessions.middleware.SessionMiddleware', 46 | 'django.middleware.common.CommonMiddleware', 47 | 'django.middleware.csrf.CsrfViewMiddleware', 48 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 49 | 'django.contrib.messages.middleware.MessageMiddleware', 50 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 51 | ] 52 | 53 | ROOT_URLCONF = 'testapp.urls' 54 | 55 | TEMPLATES = [ 56 | { 57 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 58 | 'DIRS': ["templates"], 59 | 'APP_DIRS': True, 60 | 'OPTIONS': { 61 | 'context_processors': [ 62 | 'django.template.context_processors.debug', 63 | 'django.template.context_processors.request', 64 | 'django.contrib.auth.context_processors.auth', 65 | 'django.contrib.messages.context_processors.messages', 66 | ], 67 | }, 68 | }, 69 | ] 70 | 71 | WSGI_APPLICATION = 'testapp.wsgi.application' 72 | 73 | 74 | # Database 75 | # https://docs.djangoproject.com/en/2.0/ref/settings/#databases 76 | 77 | DATABASES = { 78 | 'default': { 79 | 'ENGINE': 'django.db.backends.sqlite3', 80 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 81 | } 82 | } 83 | 84 | 85 | # Password validation 86 | # https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators 87 | 88 | AUTH_PASSWORD_VALIDATORS = [ 89 | { 90 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 91 | }, 92 | { 93 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 94 | }, 95 | { 96 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 97 | }, 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 100 | }, 101 | { 102 | 'NAME': "django_pwnedpasswords_validator.validation.PwnedPasswordValidator", 103 | 'OPTIONS': { 104 | 'error_text': "Your password was found in a data breach.", 105 | 'anonymous': True 106 | } 107 | } 108 | ] 109 | 110 | 111 | # Internationalization 112 | # https://docs.djangoproject.com/en/2.0/topics/i18n/ 113 | 114 | LANGUAGE_CODE = 'en-us' 115 | 116 | TIME_ZONE = 'UTC' 117 | 118 | USE_I18N = True 119 | 120 | USE_L10N = True 121 | 122 | USE_TZ = True 123 | 124 | 125 | # Static files (CSS, JavaScript, Images) 126 | # https://docs.djangoproject.com/en/2.0/howto/static-files/ 127 | 128 | STATIC_URL = '/static/' 129 | -------------------------------------------------------------------------------- /example/testapp/templates/register.html: -------------------------------------------------------------------------------- 1 | {{ form }} 2 | -------------------------------------------------------------------------------- /example/testapp/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path 3 | from django.urls import include 4 | 5 | from . import views 6 | 7 | urlpatterns = [ 8 | path('accounts/register', views.account_register), 9 | path('accounts/', include('django.contrib.auth.urls')), 10 | ] 11 | -------------------------------------------------------------------------------- /example/testapp/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import views as auth_views 2 | from django.shortcuts import render 3 | from django.contrib.auth.models import User 4 | 5 | from testapp import forms 6 | 7 | def account_register(request): 8 | form = forms.UserForm() 9 | if request.method == "POST": 10 | username = request.POST.get("username") 11 | if username is not None: 12 | try: 13 | instance = User.objects.get(username__iexact=username) 14 | except User.DoesNotExist: 15 | form = forms.UserForm(request.POST) 16 | if form.is_valid(): 17 | form.save() 18 | return render(request, "success.html", {}) 19 | 20 | return render(request, "register.html", {'form': form}) 21 | 22 | -------------------------------------------------------------------------------- /example/testapp/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for testapp project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testapp.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /meta/header.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lionheart/django-pwnedpasswords-validator/ac1b95eced5abacc884af8a3745d6de0a0db703a/meta/header.sketch -------------------------------------------------------------------------------- /meta/repo-banner-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lionheart/django-pwnedpasswords-validator/ac1b95eced5abacc884af8a3745d6de0a0db703a/meta/repo-banner-2.png -------------------------------------------------------------------------------- /meta/repo-banner-bottom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lionheart/django-pwnedpasswords-validator/ac1b95eced5abacc884af8a3745d6de0a0db703a/meta/repo-banner-bottom.png -------------------------------------------------------------------------------- /meta/repo-banner-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lionheart/django-pwnedpasswords-validator/ac1b95eced5abacc884af8a3745d6de0a0db703a/meta/repo-banner-small.png -------------------------------------------------------------------------------- /meta/repo-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lionheart/django-pwnedpasswords-validator/ac1b95eced5abacc884af8a3745d6de0a0db703a/meta/repo-banner.png -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [egg_info] 2 | tag_svn_revision = false 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright 2018 Lionheart Software LLC 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | from distutils.cmd import Command 19 | import os 20 | import re 21 | import unittest 22 | import runpy 23 | 24 | try: 25 | from setuptools import setup 26 | except ImportError: 27 | from distutils.core import setup 28 | 29 | metadata_filename = "django_pwnedpasswords_validator/metadata.py" 30 | metadata = runpy.run_path(metadata_filename) 31 | 32 | with open(os.path.join(os.path.dirname(__file__), "README.rst")) as file: 33 | long_description = file.read() 34 | 35 | id_regex = re.compile(r"<\#([\w-]+)>") 36 | link_regex = re.compile(r"<(\w+)>") 37 | link_alternate_regex = re.compile(r" :target: (\w+)") 38 | 39 | long_description = id_regex.sub(r"", long_description) 40 | long_description = link_regex.sub(r"", long_description) 41 | long_description = link_regex.sub(r"", long_description) 42 | long_description = link_alternate_regex.sub(r" :target: https://github.com/lionheart/django-pwnedpasswords-validator/blob/master/\1", long_description) 43 | 44 | long_description = long_description.replace("`__", "`_") 45 | 46 | classifiers = [ 47 | "Development Status :: 5 - Production/Stable", 48 | "Intended Audience :: Developers", 49 | "License :: OSI Approved :: Apache Software License", 50 | "Natural Language :: English", 51 | "Operating System :: OS Independent", 52 | "Operating System :: Unix", 53 | "Operating System :: MacOS :: MacOS X", 54 | "Programming Language :: Python :: 2.7", 55 | "Programming Language :: Python :: 3.5", 56 | "Programming Language :: Python :: 3.6", 57 | "Programming Language :: Python", 58 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content :: CGI Tools/Libraries", 59 | "Topic :: Software Development :: Libraries", 60 | "Topic :: Software Development :: Libraries :: Python Modules", 61 | "Topic :: Utilities", 62 | ] 63 | 64 | setup( 65 | author=metadata['__author__'], 66 | author_email=metadata['__email__'], 67 | classifiers=classifiers, 68 | description="A Django app that validates user passwords against the Pwned Passwords v2 API.", 69 | install_requires=["pwnedpasswords"], 70 | keywords="passwords security", 71 | license=metadata['__license__'], 72 | long_description=long_description, 73 | name='django-pwnedpasswords-validator', 74 | package_data={'': ['LICENSE', 'README.rst']}, 75 | packages=['django_pwnedpasswords_validator'], 76 | url="https://github.com/lionheart/django-pwnedpasswords-validator", 77 | download_url="https://github.com/lionheart/django-pwnedpasswords-validator/tarball/{}".format(metadata['__version__']), 78 | version=metadata['__version__'], 79 | ) 80 | --------------------------------------------------------------------------------