Import upstream version 4.4.0
Debian Janitor
2 years ago
0 | name: build | |
1 | on: | |
2 | push: | |
3 | branches: | |
4 | - master | |
5 | pull_request: | |
6 | branches: | |
7 | - master | |
8 | jobs: | |
9 | lint: | |
10 | name: lint | |
11 | runs-on: ubuntu-latest | |
12 | steps: | |
13 | - uses: actions/checkout@v2 | |
14 | - uses: actions/setup-python@v2 | |
15 | - run: python -m pip install --upgrade pip wheel | |
16 | - run: pip install tox tox-gh-actions | |
17 | - run: tox -eflake8 | |
18 | - run: tox -edocs | |
19 | tests: | |
20 | name: tests | |
21 | strategy: | |
22 | matrix: | |
23 | os: [ubuntu-latest, macos-latest, windows-latest] | |
24 | python: ['3.6', '3.7', '3.8', '3.9', 'pypy3'] | |
25 | exclude: | |
26 | # pypy3 currently fails to run on Windows | |
27 | - os: windows-latest | |
28 | python: pypy3 | |
29 | fail-fast: false | |
30 | runs-on: ${{ matrix.os }} | |
31 | steps: | |
32 | - uses: actions/checkout@v2 | |
33 | - uses: actions/setup-python@v2 | |
34 | with: | |
35 | python-version: ${{ matrix.python }} | |
36 | - run: python -m pip install --upgrade pip wheel | |
37 | - run: pip install tox tox-gh-actions | |
38 | - run: tox | |
39 | coverage: | |
40 | name: coverage | |
41 | runs-on: ubuntu-latest | |
42 | steps: | |
43 | - uses: actions/checkout@v2 | |
44 | - uses: actions/setup-python@v2 | |
45 | - run: python -m pip install --upgrade pip wheel | |
46 | - run: pip install tox tox-gh-actions codecov | |
47 | - run: tox | |
48 | - run: codecov |
0 | dist: xenial | |
0 | 1 | language: python |
1 | 2 | matrix: |
2 | 3 | include: |
3 | - python: 3.6 | |
4 | - python: 3.8 | |
4 | 5 | env: TOXENV=flake8 |
5 | 6 | - python: 2.7 |
6 | 7 | env: TOXENV=py27 |
7 | - python: 3.4 | |
8 | env: TOXENV=py34 | |
9 | - python: 3.5 | |
10 | env: TOXENV=py35 | |
11 | 8 | - python: 3.6 |
12 | 9 | env: TOXENV=py36 |
10 | - python: 3.7 | |
11 | env: TOXENV=py37 | |
12 | - python: 3.8 | |
13 | env: TOXENV=py38 | |
14 | - python: 3.9 | |
15 | env: TOXENV=py39 | |
13 | 16 | - python: pypy |
14 | 17 | env: TOXENV=pypy |
15 | 18 | - python: pypy3 |
16 | 19 | env: TOXENV=pypy3 |
17 | - python: 3.6 | |
20 | - python: 3.8 | |
18 | 21 | env: TOXENV=docs |
19 | 22 | install: |
20 | 23 | - pip install tox |
0 | # Flask-HTTPAuth Change Log | |
1 | ||
2 | ## Release 3.2.2 - 2017-01-29 | |
3 | ||
4 | - Validate authorization header in multi auth ([#51](https://github.com/miguelgrinberg/Flask-HTTPAuth/issues/51)) | |
5 | ||
6 | ## Release 3.2.1 - 2016-09-04 | |
7 | ||
8 | - Added `__version__` to top-level package | |
9 | - Added readme and license files to package | |
10 | ||
11 | ## Release 3.2.0 - 2016-08-20 | |
12 | ||
13 | - Changed license to MIT | |
14 | - Fix TCP Connection reset by peer error ([#39](https://github.com/miguelgrinberg/Flask-HTTPAuth/pull/39)) | |
15 | ||
16 | ## Release 3.1.2 - 2016-04-20 | |
17 | ||
18 | - Make password check more robust. | |
19 | ||
20 | ## Release 3.1.1 - 2016-03-24 | |
21 | ||
22 | - `MultiAuth` class did not pass parameters to decorated function. ([#35](https://github.com/miguelgrinberg/Flask-HTTPAuth/issues/35)) | |
23 | ||
24 | ## Release 3.1.0 - 2016-03-13 | |
25 | ||
26 | - Added `MultiAuth` class, to allow the combination of multiple authentication methods. | |
27 | - Added additional test for token authentication | |
28 | - Added a few examples | |
29 | ||
30 | ## Release 3.0.2 - 2016-03-11 | |
31 | ||
32 | - Invoke `verify_password` callback with no authentication when the provided authentication does not match the scheme | |
33 | ||
34 | ## Release 3.0.1 - 2016-03-09 | |
35 | ||
36 | - Prevented crash when client sends an invalid authorization header for token auth | |
37 | ||
38 | ## Release 3.0.0 - 2016-03-06 | |
39 | ||
40 | - Added token authentication support | |
41 | - Switch Travis CI builds to use tox | |
42 | - Refactored tests into separate test packages for each authentication method | |
43 | - Added explicit Python 2 and 3 classifiers to setup script | |
44 | ||
45 | ## Release 2.7.1 - 2016-02-07 | |
46 | ||
47 | - Correctly obtain nonce and opaque values in `authenticate_header` function | |
48 | - Documentation updates | |
49 | ||
50 | ## Release 2.7.0 - 2015-09-19 | |
51 | ||
52 | - Support custom authentication scheme and realm | |
53 | ||
54 | ## Release 2.6.0 - 2015-08-22 | |
55 | ||
56 | - Added callbacks for custom digest auth nonce/opaque generation | |
57 | - Documentation updates | |
58 | - Travis CI builds | |
59 | ||
60 | ## Release 2.5.0 - 2015-04-25 | |
61 | ||
62 | - In digest auth, support the client providing a pre-generated "ha1" instead of plain text password | |
63 | - Add "ha1" generation helper function for digest auth | |
64 | - Documentation updates | |
65 | ||
66 | ## Release 2.4.0 - 2015-03-01 | |
67 | ||
68 | - Support anonymous users in `verify_password` callback | |
69 | - Unit test fixes | |
70 | ||
71 | ## Release 2.3.0 - 2014-09-23 | |
72 | ||
73 | - Corrections to `hash_password` and `verify_password` decorators | |
74 | - Bypass authentication for `OPTIONS` requests | |
75 | - Pep8 compliance | |
76 | ||
77 | ## Release 2.2.1 - 2014-03-16 | |
78 | ||
79 | - Fixed documentation examples | |
80 | - Corrections to `get_password` decorator implementation | |
81 | ||
82 | ## Release 2.2.0 - 2013-11-25 | |
83 | ||
84 | - Build fixes | |
85 | ||
86 | ## Release 2.1.0 - 2013-09-27 | |
87 | ||
88 | - Support optionally passing the username to the hash password callback | |
89 | ||
90 | ## Release 2.0.0 - 2013-09-26 | |
91 | ||
92 | - Changed `auth.username` property to a `auth.username()` function | |
93 | - Documentation updates | |
94 | ||
95 | ## Release 1.1.0 - 2013-08-30 | |
96 | ||
97 | - Python 3 support | |
98 | - Documentation updates | |
99 | ||
100 | ## Release 1.0.0 - 2013-07-27 | |
101 | ||
102 | - First official release | |
103 |
0 | # Flask-HTTPAuth change log | |
1 | ||
2 | **Release 4.4.0** - 2021-05-13 | |
3 | ||
4 | - Replace `safe_str_cmp` with `hmac.compare_digest` to avoid a deprecation warning from Werkzeug [#126](https://github.com/miguelgrinberg/Flask-HTTPAuth/issues/126) ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/79e3ebf77f4ad6a56a02996a08c4517f61151d49)) (thanks **Federico Martinez**!) | |
5 | - Drop Python 2 support ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/e690ce56827de9d669718fa5d0fcda63112f8008)) | |
6 | ||
7 | **Release 4.3.0** - 2021-05-01 | |
8 | ||
9 | - Support token auth with custom header in MultiAuth class [#125](https://github.com/miguelgrinberg/Flask-HTTPAuth/issues/125) ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/6509081c72a2f92c1500b3f09aa063441ea60031)) | |
10 | - Catch `UnicodeDecodeError` when passing malformed data in authorization header [#122](https://github.com/miguelgrinberg/Flask-HTTPAuth/issues/122) ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/538569f5895834a9f7b8d4dcfd543be6fbfca37e)) (thanks **Bastian Raschke**!) | |
11 | - Fixes typo [#116](https://github.com/miguelgrinberg/Flask-HTTPAuth/issues/116) ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/9b4659e47b7e05a630f91b7e9471feef5111b503)) (thanks **Renato Oliveira**!) | |
12 | - Move builds to GitHub actions ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/588b277cae820a680199e0acf5a97e2be50c6f6c)) | |
13 | ||
14 | **Release 4.2.0** - 2020-11-16 | |
15 | ||
16 | - Allow error response to return a 200 status code [#114](https://github.com/miguelgrinberg/Flask-HTTPAuth/issues/114) ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/f3e6a5754e89cda30fa88ef8b9dfa31e1697a688)) | |
17 | - Add optional argument to MultiAuth class [#115](https://github.com/miguelgrinberg/Flask-HTTPAuth/issues/115) ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/e3c6e5fb0481c14c326460408c2d0d038adf7ddc)) (thanks **pryankster** and **Michael Wright**!) | |
18 | - Remove python 3.5 and add python 3.9 to build ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/507a7c0bfdf7da3bfb6a0cff9624295cf1119986)) | |
19 | ||
20 | **Release 4.1.0** - 2020-06-04 | |
21 | ||
22 | - Basic authentication with custom scheme ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/1aaf872716cb46330fd49e89663da1a568e54f0b)) | |
23 | ||
24 | **Release 4.0.0** - 2020-04-26 | |
25 | ||
26 | - Return user object from verify callbacks ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/51748c24f5aa53175b0f2712b814f7ea581f04e4)) | |
27 | - New role authorization support ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/8178f6dd74dab47b993ba532dd12f0cfdb5799f1)) (thanks **gemerden**!) | |
28 | - Add a custom token authorization header option ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/575b46ade7188152e1b82de84be949bf3f8a300b)) (thanks **Mohamed Feddad**!) | |
29 | - Support an optional=True argument in `login_required` decorator ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/8ecbb1157822360f5bdb24231fd50f25a6247620)) (thanks **Saif Almansoori**!) | |
30 | - Pass HTTP status code to error callback ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/fc8bcd6772b53ef5cc14cd4c6199d63cd2c71f30)) | |
31 | - More secure example of basic auth in the documentation ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/0043e138cd99c7e9fa179ee30ad2283f9b8c704f)) | |
32 | - Fix broken links in CHANGES.md and changelog template [#85](https://github.com/miguelgrinberg/Flask-HTTPAuth/issues/85) ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/96fafd43c2d0275f2d4042e95faefce24183ec02)) (thanks **Katie Smith**!) | |
33 | ||
34 | **Release 3.3.0** - 2019-05-19 | |
35 | ||
36 | - Use constant time string comparisons [#82](https://github.com/miguelgrinberg/Flask-HTTPAuth/issues/82) ([commit1](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/788d42ea9c4d536af628e0e7f4cb1fb84fc59a8e), [commit2](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/97f0e641a6d5eb34054de1ca255e932313d441ee)) (thanks **Brendan Long**!) | |
37 | - Edited and changed the usage of JWT, because in fact the code and documentation uses JWS tokens. [#79](https://github.com/miguelgrinberg/Flask-HTTPAuth/issues/79) ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/3f743c661e281d728bd2f98af8cca000a975bb8a)) (thanks **unuseless**!) | |
38 | - Documentation fix [#78](https://github.com/miguelgrinberg/Flask-HTTPAuth/issues/78) ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/c38c52326b78c91d4410f347abcd8bc49cc63ca4)) | |
39 | - Documentation improvements [#77](https://github.com/miguelgrinberg/Flask-HTTPAuth/issues/77) ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/ce5e5b4c9e8b748eba886ded5180e1e5d5036528)) | |
40 | - helper release script ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/7276d8db4b695645b01f3275addbec10418da63d)) | |
41 | ||
42 | **Release 3.2.4** - 2018-06-17 | |
43 | ||
44 | - Refactored HTTPAuth login_required [#74](https://github.com/miguelgrinberg/Flask-HTTPAuth/issues/74) ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/68ee1e7a92355ba0f3f9b48c9489a67ab762e106)) (thanks **nestedsoftware**!) | |
45 | - remove incorrect references to JWT in example application [#69](https://github.com/miguelgrinberg/Flask-HTTPAuth/issues/69) ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/a310b78db2b947ab70f3fc35c1a586d822acc7ca)) | |
46 | - Fix typo in docs [#70](https://github.com/miguelgrinberg/Flask-HTTPAuth/issues/70) ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/b6457ae5648a50df75f3c40af4b4b3f0155fc25f)) (thanks **Grey Li**!) | |
47 | - Fix documentation [#67](https://github.com/miguelgrinberg/Flask-HTTPAuth/issues/67) ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/9bd8f4b4f3574c7ef3e2fb9596bc9e9981275011)) (thanks **Eugene Rymarev**!) | |
48 | - correct spelling mistake [#56](https://github.com/miguelgrinberg/Flask-HTTPAuth/issues/56) ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/f7c5bbd1b3a53080171bbdc5f1f1842f7a825f6a)) (thanks **Edward Betts**!) | |
49 | - travis build fix for py36 ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/6e7f32984bda8b82200793c1b3ec44ff3df3ad2b)) | |
50 | ||
51 | **Release 3.2.3** - 2017-06-05 | |
52 | ||
53 | - Include docs and tests in pypi source tarball [#55](https://github.com/miguelgrinberg/Flask-HTTPAuth/issues/55) ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/054810ee351148b14571ba0a89ec17a543c35078)) (thanks **Chandan Kumar**!) | |
54 | ||
55 | **Release 3.2.2** - 2017-01-30 | |
56 | ||
57 | - Validate authorization header in multi auth [#51](https://github.com/miguelgrinberg/Flask-HTTPAuth/issues/51) ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/7a895d676a1b6998f58b61a177286b62dc2872f5)) | |
58 | - index.rst: Add a missing variable in a code snippet [#49](https://github.com/miguelgrinberg/Flask-HTTPAuth/issues/49) ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/f7fe976bbdc699e8bafaed729dfdd74d2b27d7db)) (thanks **Baptiste Fontaine**!) | |
59 | ||
60 | **Release 3.2.1** - 2016-09-04 | |
61 | ||
62 | - add `__version__` to package ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/d188450987f226568fe0cdee0b6d480b375af64a)) | |
63 | - Add readme and license files to the built package [#45](https://github.com/miguelgrinberg/Flask-HTTPAuth/issues/45) ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/1c35bec606f147bb23725d6ff3b0411f06828492)) | |
64 | ||
65 | **Release 3.2.0** - 2016-08-20 | |
66 | ||
67 | - Fix TCP Connection reset by peer error [#39](https://github.com/miguelgrinberg/Flask-HTTPAuth/issues/39) ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/94f6c6d5a4866a43ff4f269eb351dce6232791a2)) (thanks **Joe Kemp**!) | |
68 | ||
69 | **Release 3.1.2** - 2016-04-21 | |
70 | ||
71 | - Add robustness to password check ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/051fd88ee36a21a13255b4ec69e172c9ae4ad46d)) | |
72 | ||
73 | **Release 3.1.1** - 2016-03-24 | |
74 | ||
75 | - pass params to view function in MultiAuth [#36](https://github.com/miguelgrinberg/Flask-HTTPAuth/issues/36) ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/319974602e55529006b9a8a4fde04ef08e042e83)) (thanks **vovanz**!) | |
76 | - add examples to flake8 build ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/61b1b71b3b29f2936ac6a2077883da1faeaad09f)) | |
77 | - Added multi auth tests ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/c443e7ebcc227fd3690c2cf943d414087d7b931d)) | |
78 | - removed dead code ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/4d2232e2a77f5e10e1731936f4ac64439049b220)) | |
79 | ||
80 | **Release 3.1.0** - 2016-03-13 | |
81 | ||
82 | - examples ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/609806a1c10264818e08ba0ce9b7babeaf101656)) | |
83 | - Added support for multiple authentication methods ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/6c3f94d9eda85b78a8c36cd5e05d6d9836bee2d0)) | |
84 | - Added change log ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/8b427b962114a6ef13badaf8f2f1b396c540955a)) | |
85 | - Add additional token auth test ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/29edb1948f086babbd1a9e0c87a0a35c05f0a63b)) | |
86 | ||
87 | **Release 3.0.2** - 2016-03-12 | |
88 | ||
89 | - Let callback decide what to do when authentication type does not match ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/b942f980970d2e387a80f68de4ea2bb8728b149c)) | |
90 | ||
91 | **Release 3.0.1** - 2016-03-09 | |
92 | ||
93 | - Catching exception when Authorization header is empty ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/88d073e05b56b810feb447d1c9cee7a9a9ac9b1b)) (thanks **Kari Hreinsson**!) | |
94 | - Documentation fix, validate_token() -> verify_token() ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/f4b41d736311638978c95c9b5fd458063a009280)) (thanks **Kari Hreinsson**!) | |
95 | ||
96 | **Release 3.0.0** - 2016-03-07 | |
97 | ||
98 | - documentation for new token auth ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/c0ae42df517a45be87f419cbb7f8002228a1e83c)) | |
99 | - switch travis build to use tox ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/00fdebce667e1dbbc5b342a21804cb6ab3b4f417)) | |
100 | - token auth support, plus test reorg ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/aac866de14c68a4d17d3098f8e96102e837add1d)) | |
101 | - Added explicity Python 2 & 3 version classifiers to package ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/a6f50e7be6f13bb814c47fe8a3a44cd34138f87e)) | |
102 | ||
103 | **Release 2.7.1** - 2016-02-07 | |
104 | ||
105 | - Remove session dependency in authenticate_header [#31](https://github.com/miguelgrinberg/Flask-HTTPAuth/issues/31) ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/8a84c52d2166e7fdfa26b89dfd2df3340787de94)) (thanks **Paweł Stiasny**!) | |
106 | - Add Install Notes ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/0ff88331c9724999d8f283d79fe95de949e64438)) (thanks **Michael Washburn Jr**!) | |
107 | - Add syntax highlighting to the README [#28](https://github.com/miguelgrinberg/Flask-HTTPAuth/issues/28) ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/5c058b5165cdbc6a869d68410ef2d25e7802d602)) (thanks **Josh Friend**!) | |
108 | ||
109 | **Release 2.7.0** - 2015-09-20 | |
110 | ||
111 | - Support custom authentication scheme and realm ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/bf12f959bba24a2f3d7d799d1b57ef3a5f1001e8)) | |
112 | ||
113 | **Release 2.6.0** - 2015-08-23 | |
114 | ||
115 | - Added information on how to implement digest authentication securely ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/fb02625ca0f7694d8e744e0b3d2c8d4ffcc4d7cd)) | |
116 | - Allow for custom nonce/opaque generation [#24](https://github.com/miguelgrinberg/Flask-HTTPAuth/issues/24) ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/ddaa3b6461705d107655c7f87f90d7ba962d2a84)) (thanks **Matt Haggard**!) | |
117 | - fixed tests to work with python 2.6 ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/5e85b27a06285fb5bd591f9f65a8a0bebc4a34f2)) | |
118 | - added travis ci badge ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/ef354fd07abd08137beba6362debdcb4ef23baf6)) | |
119 | ||
120 | **release 2.5.0** - 2015-04-26 | |
121 | ||
122 | - documentation changes ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/5c98ed8370355a60e22e017a79d5575adadb9c07)) | |
123 | - documentation for stored ha1 feature ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/37fd9288abb4f11abf9f93303d1bce4e6cfc3c19)) | |
124 | - Include notes for nginx ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/ed8b4a3c954240cde0c66af3d6dae37df48ba976)) (thanks **Erik Stephens**!) | |
125 | - Include notes for nginx as well ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/5bccbae862cbf1ca7d02f717b076aca86b1456e5)) (thanks **Erik Stephens**!) | |
126 | - Update docs with WSGI notes ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/9ddd55f0bcb793a49675274dc22ae15122a8a1ff)) (thanks **Erik Stephens**!) | |
127 | - Update README with WSGI notes ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/af5fa26dc73d401de7760ba3dcd61828c2e548dd)) (thanks **Erik Stephens**!) | |
128 | - Modified documents and readme for correct import statement [#19](https://github.com/miguelgrinberg/Flask-HTTPAuth/issues/19) ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/b75737593f3d97b18620440e7e41ee9b71b23f11)) (thanks **Aayush Kasurde**!) | |
129 | ||
130 | **release 2.4.0** - 2015-03-02 | |
131 | ||
132 | - Support anonymous users in verify_password callback ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/5c5396bbb7af540a7aff786ce3282657566045f2)) | |
133 | - Add HA1 generation function to HTTPDigestAuth class ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/4f4aed3ed3fa5e96a1a052e4414f14d1fc49b8bb)) (thanks **Pawel Szczurko**!) | |
134 | - Fix unit test url routes ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/a490a521a17313ce82bfe886912b1620166eb6dd)) (thanks **Pawel Szczurko**!) | |
135 | - Add option to use ha1 combination as password instead of plain text password ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/c84429f541ed0069f40fb901dcb3df44b801c9a5)) (thanks **Pawel Szczurko**!) | |
136 | - removed extra strip() calls in unit tests ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/fc34cc5020168ca3824cc4a740b2010bb3132abf)) | |
137 | ||
138 | **release 2.3.0** - 2014-09-23 | |
139 | ||
140 | - pep8 ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/4657d5b37e50483ecccabf0887ea417d3b94ea0a)) | |
141 | - Fixed problem with couple of decorator that destroy function they decorate [#11](https://github.com/miguelgrinberg/Flask-HTTPAuth/issues/11) ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/0adf45bec7e5fb04a0e14e13396fd867879026b4)) (thanks **Nemanja Trifunovic**!) | |
142 | - Ignore authentication headers for OPTIONS ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/044b7d4a44425a4b9d02280b80988e8986641a0d)) (thanks **Henrique Carvalho Alves**!) | |
143 | ||
144 | **release 2.2.1** - 2014-03-17 | |
145 | ||
146 | - [#5](https://github.com/miguelgrinberg/Flask-HTTPAuth/issues/5): correct handling of None return from get_password callback ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/b94dc8e5fb6c914fdf971085b329bf9ad848a8f5)) | |
147 | - [#5](https://github.com/miguelgrinberg/Flask-HTTPAuth/issues/5) ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/051195d68d8aaf6d9e53d14d69a59afd84f24821)) | |
148 | - Fixed problem when get_password decorator destroys function it decorates [#4](https://github.com/miguelgrinberg/Flask-HTTPAuth/issues/4) ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/0cbee173e96f8e1a533e7d82b5b1fa1bfce3cd04)) (thanks **Nemanja Trifunovic**!) | |
149 | - custom password verification callback ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/33d60f21a6e64f1b2df24ea5035164110979d8ab)) | |
150 | ||
151 | **version 2.1.0** - 2013-09-28 | |
152 | ||
153 | - pass the username to the hash password callback ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/13075ec4dbe4cb733f4f433e1e25e8a180fce1f6)) | |
154 | ||
155 | **Release 2.0.0** - 2013-09-26 | |
156 | ||
157 | - changed auth.username to auth.username() ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/5168a5f703552ec092e3fef9e087052e35fb6ff0)) | |
158 | - 2.0 documentation update ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/e668f59cb674e45891b7d9548e5af3028f2fd22d)) | |
159 | ||
160 | **Release 1.1.0** - 2013-08-30 | |
161 | ||
162 | - python 3 support ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/c13ff0a4c1e5922a635ea7c877a2ef6079ddb4e6)) | |
163 | - documentation update ([commit](https://github.com/miguelgrinberg/Flask-HTTPAuth/commit/c468e1c084e5c25dcaa85b45e5abeb88fbc09420)) | |
164 | ||
165 | **Release 1.0.0** - 2013-07-27 | |
166 | ||
167 | - First official release! |
0 | 0 | Flask-HTTPAuth |
1 | 1 | ============== |
2 | 2 | |
3 | [![Build Status](https://travis-ci.org/miguelgrinberg/Flask-HTTPAuth.png?branch=master)](https://travis-ci.org/miguelgrinberg/Flask-HTTPAuth) | |
3 | [![Build status](https://github.com/miguelgrinberg/Flask-HTTPAuth/workflows/build/badge.svg)](https://github.com/miguelgrinberg/Flask-HTTPAuth/actions) [![codecov](https://codecov.io/gh/miguelgrinberg/Flask-HTTPAuth/branch/master/graph/badge.svg?token=KeU2002DHo)](https://codecov.io/gh/miguelgrinberg/Flask-HTTPAuth) | |
4 | 4 | |
5 | 5 | Simple extension that provides Basic and Digest HTTP authentication for Flask routes. |
6 | 6 | |
17 | 17 | ```python |
18 | 18 | from flask import Flask |
19 | 19 | from flask_httpauth import HTTPBasicAuth |
20 | from werkzeug.security import generate_password_hash, check_password_hash | |
20 | 21 | |
21 | 22 | app = Flask(__name__) |
22 | 23 | auth = HTTPBasicAuth() |
23 | 24 | |
24 | 25 | users = { |
25 | "john": "hello", | |
26 | "susan": "bye" | |
26 | "john": generate_password_hash("hello"), | |
27 | "susan": generate_password_hash("bye") | |
27 | 28 | } |
28 | 29 | |
29 | @auth.get_password | |
30 | def get_pw(username): | |
31 | if username in users: | |
32 | return users.get(username) | |
33 | return None | |
30 | @auth.verify_password | |
31 | def verify_password(username, password): | |
32 | if username in users and \ | |
33 | check_password_hash(users.get(username), password): | |
34 | return username | |
34 | 35 | |
35 | 36 | @app.route('/') |
36 | 37 | @auth.login_required |
37 | 38 | def index(): |
38 | return "Hello, %s!" % auth.username() | |
39 | return "Hello, %s!" % auth.current_user() | |
39 | 40 | |
40 | 41 | if __name__ == '__main__': |
41 | 42 | app.run() |
79 | 80 | |
80 | 81 | - [Documentation](http://flask-httpauth.readthedocs.io/en/latest/) |
81 | 82 | - [PyPI](https://pypi.org/project/Flask-HTTPAuth) |
82 | - [Change log](https://github.com/miguelgrinberg/Flask-HTTPAuth/blob/master/CHANGELOG.md) | |
83 | - [Change log](https://github.com/miguelgrinberg/Flask-HTTPAuth/blob/master/CHANGES.md) |
0 | import datetime | |
1 | import re | |
2 | import sys | |
3 | import git | |
4 | ||
5 | URL = 'https://github.com/miguelgrinberg/Flask-HTTPAuth' | |
6 | merges = {} | |
7 | ||
8 | ||
9 | def format_message(commit): | |
10 | if commit.message.startswith('Version '): | |
11 | return '' | |
12 | if '#nolog' in commit.message: | |
13 | return '' | |
14 | if commit.message.startswith('Merge pull request'): | |
15 | pr = commit.message.split('#')[1].split(' ')[0] | |
16 | message = ' '.join([line for line in [line.strip() for line in commit.message.split('\n')[1:]] if line]) | |
17 | merges[message] = pr | |
18 | return '' | |
19 | if commit.message.startswith('Release '): | |
20 | return '\n**{message}** - {date}\n'.format( | |
21 | message=commit.message.strip(), | |
22 | date=datetime.datetime.fromtimestamp(commit.committed_date).strftime('%Y-%m-%d')) | |
23 | message = ' '.join([line for line in [line.strip() for line in commit.message.split('\n')] if line]) | |
24 | if message in merges: | |
25 | message += ' #' + merges[message] | |
26 | message = re.sub('\\(.*(#[0-9]+)\\)', '\\1', message) | |
27 | message = re.sub('Fixes (#[0-9]+)', '\\1', message) | |
28 | message = re.sub('fixes (#[0-9]+)', '\\1', message) | |
29 | message = re.sub('#([0-9]+)', '[#\\1]({url}/issues/\\1)'.format(url=URL), message) | |
30 | message += ' ([commit]({url}/commit/{sha}))'.format(url=URL, sha=str(commit)) | |
31 | if commit.author.name != 'Miguel Grinberg': | |
32 | message += ' (thanks **{name}**!)'.format(name=commit.author.name) | |
33 | return '- ' + message | |
34 | ||
35 | ||
36 | def main(all=False): | |
37 | repo = git.Repo() | |
38 | ||
39 | for commit in repo.iter_commits(): | |
40 | if not all and commit.message.startswith('Release '): | |
41 | break | |
42 | message = format_message(commit) | |
43 | if message: | |
44 | print(message) | |
45 | ||
46 | ||
47 | if __name__ == '__main__': | |
48 | main(all=len(sys.argv) > 1 and sys.argv[1] == 'all') |
0 | #!/bin/bash -ex | |
1 | ||
2 | VERSION="$1" | |
3 | VERSION_FILE=flask_httpauth.py | |
4 | ||
5 | if [[ "$VERSION" == "" ]]; then | |
6 | echo "Usage: $0 <version>" | |
7 | fi | |
8 | ||
9 | # update change log | |
10 | head -n 2 CHANGES.md > _CHANGES.md | |
11 | echo "**Release $VERSION** - $(date +%F)" >> _CHANGES.md | |
12 | echo "" >> _CHANGES.md | |
13 | pip install gitpython | |
14 | python bin/mkchangelog.py >> _CHANGES.md | |
15 | echo "" >> _CHANGES.md | |
16 | len=$(wc -l < CHANGES.md) | |
17 | tail -n $(expr $len - 2) CHANGES.md >> _CHANGES.md | |
18 | vim _CHANGES.md | |
19 | set +e | |
20 | grep -q ABORT _CHANGES.md | |
21 | if [[ "$?" == "0" ]]; then | |
22 | rm _CHANGES.md | |
23 | echo "Aborted." | |
24 | exit 1 | |
25 | fi | |
26 | set -e | |
27 | mv _CHANGES.md CHANGES.md | |
28 | ||
29 | sed -i "" "s/^__version__ = '.*'$/__version__ = '$VERSION'/" $VERSION_FILE | |
30 | rm -rf dist | |
31 | pip install --upgrade pip wheel twine | |
32 | python setup.py sdist bdist_wheel --universal | |
33 | ||
34 | git add $VERSION_FILE CHANGES.md | |
35 | git commit -m "Release $VERSION" | |
36 | git tag -f v$VERSION | |
37 | git push --tags origin master | |
38 | ||
39 | read -p "Press any key to submit to PyPI or Ctrl-C to abort..." -n1 -s | |
40 | twine upload dist/* | |
41 | ||
42 | NEW_VERSION="${VERSION%.*}.$((${VERSION##*.}+1))dev" | |
43 | sed -i "" "s/^__version__ = '.*'$/__version__ = '$NEW_VERSION'/" $VERSION_FILE | |
44 | git add $VERSION_FILE | |
45 | git commit -m "Version $NEW_VERSION" | |
46 | git push origin master | |
47 | echo "Development is now open on version $NEW_VERSION!" |
5 | 5 | Welcome to Flask-HTTPAuth's documentation! |
6 | 6 | ========================================== |
7 | 7 | |
8 | **Flask-HTTPAuth** is a simple extension that simplifies the use of HTTP authentication with Flask routes. | |
9 | ||
10 | Basic authentication example | |
11 | ---------------------------- | |
8 | **Flask-HTTPAuth** is a Flask extension that simplifies the use of HTTP authentication with Flask routes. | |
9 | ||
10 | Basic authentication examples | |
11 | ----------------------------- | |
12 | 12 | |
13 | 13 | The following example application uses HTTP Basic authentication to protect route ``'/'``:: |
14 | 14 | |
15 | from flask import Flask | |
16 | from flask_httpauth import HTTPBasicAuth | |
17 | ||
18 | app = Flask(__name__) | |
19 | auth = HTTPBasicAuth() | |
20 | ||
21 | users = { | |
22 | "john": "hello", | |
23 | "susan": "bye" | |
24 | } | |
25 | ||
26 | @auth.get_password | |
27 | def get_pw(username): | |
28 | if username in users: | |
29 | return users.get(username) | |
30 | return None | |
31 | ||
32 | @app.route('/') | |
33 | @auth.login_required | |
34 | def index(): | |
35 | return "Hello, %s!" % auth.username() | |
36 | ||
37 | if __name__ == '__main__': | |
38 | app.run() | |
39 | ||
40 | The ``get_password`` callback needs to return the password associated with the username given as argument. Flask-HTTPAuth will allow access only if ``get_password(username) == password``. | |
41 | ||
42 | If the passwords are stored hashed in the user database then an additional callback is needed:: | |
43 | ||
44 | @auth.hash_password | |
45 | def hash_pw(password): | |
46 | return md5(password).hexdigest() | |
47 | ||
48 | When the ``hash_password`` callback is provided access will be granted when ``get_password(username) == hash_password(password)``. | |
49 | ||
50 | If the hashing algorithm requires the username to be known then the callback can take two arguments instead of one:: | |
51 | ||
52 | @auth.hash_password | |
53 | def hash_pw(username, password): | |
54 | salt = get_salt(username) | |
55 | return hash(password, salt) | |
56 | ||
57 | For the most degree of flexibility the `get_password` and `hash_password` callbacks can be replaced with `verify_password`:: | |
58 | ||
59 | @auth.verify_password | |
60 | def verify_pw(username, password): | |
61 | return call_custom_verify_function(username, password) | |
62 | ||
63 | In the examples directory you can find an example called `basic_auth.py` that shows how a `verify_password` callback can be used to securely work with hashed passwords. | |
15 | from flask import Flask | |
16 | from flask_httpauth import HTTPBasicAuth | |
17 | from werkzeug.security import generate_password_hash, check_password_hash | |
18 | ||
19 | app = Flask(__name__) | |
20 | auth = HTTPBasicAuth() | |
21 | ||
22 | users = { | |
23 | "john": generate_password_hash("hello"), | |
24 | "susan": generate_password_hash("bye") | |
25 | } | |
26 | ||
27 | @auth.verify_password | |
28 | def verify_password(username, password): | |
29 | if username in users and \ | |
30 | check_password_hash(users.get(username), password): | |
31 | return username | |
32 | ||
33 | @app.route('/') | |
34 | @auth.login_required | |
35 | def index(): | |
36 | return "Hello, {}!".format(auth.current_user()) | |
37 | ||
38 | if __name__ == '__main__': | |
39 | app.run() | |
40 | ||
41 | The function decorated with the ``verify_password`` decorator receives the username and password sent by the client. If the credentials belong to a user, then the function should return the user object. If the credentials are invalid the functon can return ``None`` or ``False``. The user object can then be queried from the ``current_user()`` method of the authentication instance. | |
64 | 42 | |
65 | 43 | Digest authentication example |
66 | 44 | ----------------------------- |
67 | 45 | |
68 | The following example is similar to the previous one, but HTTP Digest authentication is used:: | |
46 | The following example uses HTTP Digest authentication:: | |
69 | 47 | |
70 | 48 | from flask import Flask |
71 | 49 | from flask_httpauth import HTTPDigestAuth |
88 | 66 | @app.route('/') |
89 | 67 | @auth.login_required |
90 | 68 | def index(): |
91 | return "Hello, %s!" % auth.username() | |
69 | return "Hello, {}!".format(auth.username()) | |
92 | 70 | |
93 | 71 | if __name__ == '__main__': |
94 | 72 | app.run() |
124 | 102 | |
125 | 103 | For information of what the ``nonce`` and ``opaque`` values are and how they are used in digest authentication, consult `RFC 2617 <http://tools.ietf.org/html/rfc2617#section-3.2.1>`_. |
126 | 104 | |
127 | Token Authentication Scheme Example | |
128 | ----------------------------------- | |
105 | Token Authentication Example | |
106 | ---------------------------- | |
129 | 107 | |
130 | 108 | The following example application uses a custom HTTP authentication scheme to protect route ``'/'`` with a token:: |
131 | 109 | |
133 | 111 | from flask_httpauth import HTTPTokenAuth |
134 | 112 | |
135 | 113 | app = Flask(__name__) |
136 | auth = HTTPTokenAuth(scheme='Token') | |
114 | auth = HTTPTokenAuth(scheme='Bearer') | |
137 | 115 | |
138 | 116 | tokens = { |
139 | 117 | "secret-token-1": "john", |
143 | 121 | @auth.verify_token |
144 | 122 | def verify_token(token): |
145 | 123 | if token in tokens: |
146 | g.current_user = tokens[token] | |
147 | return True | |
148 | return False | |
124 | return tokens[token] | |
149 | 125 | |
150 | 126 | @app.route('/') |
151 | 127 | @auth.login_required |
152 | 128 | def index(): |
153 | return "Hello, %s!" % g.current_user | |
129 | return "Hello, {}!".format(auth.current_user()) | |
154 | 130 | |
155 | 131 | if __name__ == '__main__': |
156 | 132 | app.run() |
157 | 133 | |
158 | The ``HTTPTokenAuth`` is a generic authentication handler that can be used with non-standard authentication schemes, with the scheme name given as an argument in the constructor. In the above example, the ``WWW-Authenticate`` header provided by the server will use ``Token`` as scheme:: | |
159 | ||
160 | WWW-Authenticate: Token realm="Authentication Required" | |
161 | ||
162 | The ``verify_token`` callback receives the authentication credentials provided by the client on the ``Authorization`` header. This can be a simple token, or can contain multiple arguments, which the function will have to parse and extract from the string. | |
163 | ||
164 | In the examples directory you can find a complete example that uses JWT tokens. | |
134 | The ``HTTPTokenAuth`` is a generic authentication handler that can be used with non-standard authentication schemes, with the scheme name given as an argument in the constructor. In the above example, the ``WWW-Authenticate`` header provided by the server will use ``Bearer`` as scheme:: | |
135 | ||
136 | WWW-Authenticate: Bearer realm="Authentication Required" | |
137 | ||
138 | The ``verify_token`` callback receives the authentication credentials provided by the client on the ``Authorization`` header. This can be a simple token, or can contain multiple arguments, which the function will have to parse and extract from the string. As with the ``verify_password``, the function should return the user object if the token is valid. | |
139 | ||
140 | In the examples directory you can find a complete example that uses JWS tokens. JWS tokens are similar to JWT tokens. However using JWT tokens would require an external dependency. | |
165 | 141 | |
166 | 142 | Using Multiple Authentication Schemes |
167 | 143 | ------------------------------------- |
168 | 144 | |
169 | Applications sometimes need to support a combination of authentication methods. For example, a web application could be authenticated by sending client id and secret over basic authentication, while third party API clients use a JWT bearer token. The `MultiAuth` class allows you to protect a route with more than one authentication object. To grant access to the endpoint, one of the authentication methods must validate. | |
145 | Applications sometimes need to support a combination of authentication | |
146 | methods. For example, a web application could be authenticated by | |
147 | sending client id and secret over basic authentication, while third | |
148 | party API clients use a JWS or JWT bearer token. The `MultiAuth` class allows you to protect a route with more than one authentication object. To grant access to the endpoint, one of the authentication methods must validate. | |
170 | 149 | |
171 | 150 | In the examples directory you can find a complete example that uses basic and token authentication. |
151 | ||
152 | User Roles | |
153 | ---------- | |
154 | ||
155 | Flask-HTTPAuth includes a simple role-based authentication system that can optionally be added to provide an additional layer of granularity in filtering accesses to routes. To enable role support, write a function that returns the list of roles for a given user and decorate it with the ``get_user_roles`` decorator:: | |
156 | ||
157 | @auth.get_user_roles | |
158 | def get_user_roles(user): | |
159 | return user.get_roles() | |
160 | ||
161 | To restrict access to a route to users having a given role, add the ``role`` argument to the ``login_required`` decorator:: | |
162 | ||
163 | @app.route('/admin') | |
164 | @auth.login_required(role='admin') | |
165 | def admins_only(): | |
166 | return "Hello {}, you are an admin!".format(auth.current_user()) | |
167 | ||
168 | The ``role`` argument can take a list of roles, in which case users who have any of the given roles will be granted access:: | |
169 | ||
170 | @app.route('/admin') | |
171 | @auth.login_required(role=['admin', 'moderator']) | |
172 | def admins_only(): | |
173 | return "Hello {}, you are an admin or a moderator!".format(auth.current_user()) | |
174 | ||
175 | In the most advanced usage, users can be filtered by having multiple roles:: | |
176 | ||
177 | @app.route('/admin') | |
178 | @auth.login_required(role=['user', ['moderator', 'contributor']]) | |
179 | def admins_only(): | |
180 | return "Hello {}, you are a user or a moderator/contributor!".format(auth.current_user()) | |
172 | 181 | |
173 | 182 | Deployment Considerations |
174 | 183 | ------------------------- |
175 | 184 | |
176 | 185 | Be aware that some web servers do not pass the ``Authorization`` headers to the WSGI application by default. For example, if you use Apache with mod_wsgi, you have to set option ``WSGIPassAuthorization On`` as `documented here <https://code.google.com/p/modwsgi/wiki/ConfigurationDirectives#WSGIPassAuthorization/>`_. |
177 | 186 | |
187 | Deprecated Basic Authentication Options | |
188 | --------------------------------------- | |
189 | ||
190 | Before the ``verify_password`` described above existed there were other simpler mechanisms for implementing basic authentication. While these are deprecated they are still maintained. However, the ``verify_password`` callback should be preferred as it provides greater security and flexibility. | |
191 | ||
192 | The ``get_password`` callback needs to return the password associated with the username given as argument. Flask-HTTPAuth will allow access only if ``get_password(username) == password``. Example:: | |
193 | ||
194 | @auth.get_password | |
195 | def get_password(username): | |
196 | return get_password_for_username(username) | |
197 | ||
198 | Using this callback alone is in general not a good idea because it requires passwords to be available in plaintext in the server. In the more likely scenario that the passwords are stored hashed in a user database, then an additional callback is needed to define how to hash a password:: | |
199 | ||
200 | @auth.hash_password | |
201 | def hash_pw(password): | |
202 | return hash_password(password) | |
203 | ||
204 | In this example, you have to replace ``hash_password()`` with the specific hashing function used in your application. When the ``hash_password`` callback is provided, access will be granted when ``get_password(username) == hash_password(password)``. | |
205 | ||
206 | If the hashing algorithm requires the username to be known then the callback can take two arguments instead of one:: | |
207 | ||
208 | @auth.hash_password | |
209 | def hash_pw(username, password): | |
210 | salt = get_salt(username) | |
211 | return hash_password(password, salt) | |
212 | ||
178 | 213 | API Documentation |
179 | 214 | ----------------- |
180 | 215 | |
192 | 227 | |
193 | 228 | The ``realm`` argument can be used to provide an application defined realm with the ``WWW-Authenticate`` header. |
194 | 229 | |
230 | .. method:: verify_password(verify_password_callback) | |
231 | ||
232 | If defined, this callback function will be called by the framework to verify that the username and password combination provided by the client are valid. The callback function takes two arguments, the username and the password. It must return the user object if credentials are valid, or ``True`` if a user object is not available. In case of failed authentication, it should return ``None`` or ``False``. Example usage:: | |
233 | ||
234 | @auth.verify_password | |
235 | def verify_password(username, password): | |
236 | user = User.query.filter_by(username).first() | |
237 | if user and passlib.hash.sha256_crypt.verify(password, user.password_hash): | |
238 | return user | |
239 | ||
240 | If this callback is defined, it is also invoked when the request does not have the ``Authorization`` header with user credentials, and in this case both the ``username`` and ``password`` arguments are set to empty strings. The application can opt to return ``True`` in this case and that will allow anonymous users access to the route. The callback function can indicate that the user is anonymous by writing a state variable to ``flask.g`` or by checking if ``auth.current_user()`` is ``None``. | |
241 | ||
242 | Note that when a ``verify_password`` callback is provided the ``get_password`` and ``hash_password`` callbacks are not used. | |
243 | ||
244 | .. method:: get_user_roles(roles_callback) | |
245 | ||
246 | If defined, this callback function will be called by the framework to obtain the roles assigned to a given user. The callback function takes a single argument, the user for which roles are requested. The user object passed to this function will be the one returned by the ``verify_callback`` function. The function should return the role or list of roles that belong to the user. Example:: | |
247 | ||
248 | @auth.get_user_roles | |
249 | def get_user_roles(user): | |
250 | return user.get_roles() | |
251 | ||
195 | 252 | .. method:: get_password(password_callback) |
196 | 253 | |
197 | This callback function will be called by the framework to obtain the password for a given user. Example:: | |
254 | *Deprecated* This callback function will be called by the framework to obtain the password for a given user. Example:: | |
198 | 255 | |
199 | 256 | @auth.get_password |
200 | 257 | def get_password(username): |
202 | 259 | |
203 | 260 | .. method:: hash_password(hash_password_callback) |
204 | 261 | |
205 | If defined, this callback function will be called by the framework to apply a custom hashing algorithm to the password provided by the client. If this callback isn't provided the password will be checked unchanged. The callback can take one or two arguments. The one argument version receives the password to hash, while the two argument version receives the username and the password in that order. Example single argument callback:: | |
262 | *Deprecated* If defined, this callback function will be called by the framework to apply a custom hashing algorithm to the password provided by the client. If this callback isn't provided the password will be checked unchanged. The callback can take one or two arguments. The one argument version receives the password to hash, while the two argument version receives the username and the password in that order. Example single argument callback:: | |
206 | 263 | |
207 | 264 | @auth.hash_password |
208 | 265 | def hash_password(password): |
215 | 272 | salt = get_salt(username) |
216 | 273 | return hash(password, salt) |
217 | 274 | |
218 | .. method:: verify_password(verify_password_callback) | |
219 | ||
220 | If defined, this callback function will be called by the framework to verify that the username and password combination provided by the client are valid. The callback function takes two arguments, the username and the password and must return ``True`` or ``False``. Example usage:: | |
221 | ||
222 | @auth.verify_password | |
223 | def verify_password(username, password): | |
224 | user = User.query.filter_by(username).first() | |
225 | if not user: | |
226 | return False | |
227 | return passlib.hash.sha256_crypt.verify(password, user.password_hash) | |
228 | ||
229 | If this callback is defined, it is also invoked when the request does not have the ``Authorization`` header with user credentials, and in this case both the ``username`` and ``password`` arguments are set to empty strings. The client can opt to return ``True`` and that will allow anonymous users access to the route. The callback function can indicate that the user is anonymous by writing a state variable to ``flask.g``, which the route can then check to generate an appropriate response. | |
230 | ||
231 | Note that when a ``verify_password`` callback is provided the ``get_password`` and ``hash_password`` callbacks are not used. | |
232 | ||
233 | 275 | .. method:: error_handler(error_callback) |
234 | 276 | |
235 | If defined, this callback function will be called by the framework when it is necessary to send an authentication error back to the client. The return value from this function can be the body of the response as a string or it can also be a response object created with ``make_response``. If this callback isn't provided a default error response is generated. Example:: | |
277 | If defined, this callback function will be called by the framework when it is necessary to send an authentication error back to the client. The function can take one argument, the status code of the error, which can be 401 (incorrect credentials) or 403 (correct, but insufficient credentials). To preserve compatiiblity with older releases of this package, the function can also be defined without arguments. The return value from this function must by any accepted response type in Flask routes. If this callback isn't provided a default error response is generated. Example:: | |
236 | 278 | |
237 | 279 | @auth.error_handler |
238 | def auth_error(): | |
239 | return "<h1>Access Denied</h1>" | |
280 | def auth_error(status): | |
281 | return "Access Denied", status | |
240 | 282 | |
241 | 283 | .. method:: login_required(view_function_callback) |
242 | 284 | |
247 | 289 | def private_page(): |
248 | 290 | return "Only for authorized people!" |
249 | 291 | |
250 | .. method:: username() | |
251 | ||
252 | A view function that is protected with this class can access the logged username through this method. Example:: | |
292 | An optional ``role`` argument can be given to further restrict access by roles. Example:: | |
293 | ||
294 | @app.route('/private') | |
295 | @auth.login_required(role='admin') | |
296 | def private_page(): | |
297 | return "Only for admins!" | |
298 | ||
299 | An optional ``optional`` argument can be set to ``True`` to allow the route to execute also when authentication is not included with the request, in which case ``auth.current_user()`` will be set to ``None``. Example:: | |
300 | ||
301 | @app.route('/private') | |
302 | @auth.login_required(optional=True) | |
303 | def private_page(): | |
304 | user = auth.current_user() | |
305 | return "Hello {}!".format(user.name if user is not None else 'anonymous') | |
306 | ||
307 | .. method:: current_user() | |
308 | ||
309 | The user object returned by the ``verify_password`` callback on successful authentication. If no user is returned by the callback, this is set to the username passed by the client. Example:: | |
253 | 310 | |
254 | 311 | @app.route('/') |
255 | 312 | @auth.login_required |
256 | 313 | def index(): |
257 | return "Hello, %s!" % auth.username() | |
258 | ||
259 | .. class:: flask_httpauth.HTTPDigestAuth | |
260 | ||
261 | This class handles HTTP Digest authentication for Flask routes. The ``SECRET_KEY`` configuration must be set in the Flask application to enable the session to work. Flask by default stores user sessions in the client as secure cookies, so the client must be able to handle cookies. To support clients that are not web browsers or that cannot handle cookies a `session interface <http://flask.pocoo.org/docs/api/#flask.Flask.session_interface>`_ that writes sessions in the server must be used. | |
314 | user = auth.current_user() | |
315 | return "Hello, {}!".format(user.name) | |
316 | ||
317 | .. method:: username() | |
318 | ||
319 | *Deprecated* A view function that is protected with this class can access the logged username through this method. Example:: | |
320 | ||
321 | @app.route('/') | |
322 | @auth.login_required | |
323 | def index(): | |
324 | return "Hello, {}!".format(auth.username()) | |
325 | ||
326 | .. class:: HTTPDigestAuth | |
327 | ||
328 | This class handles HTTP Digest authentication for Flask routes. The ``SECRET_KEY`` configuration must be set in the Flask application to enable the session to work. Flask by default stores user sessions in the client as secure cookies, so the client must be able to handle cookies. To make this authentication method secure, a `session interface <http://flask.pocoo.org/docs/api/#flask.Flask.session_interface>`_ that writes sessions in the server must be used. | |
262 | 329 | |
263 | 330 | .. method:: __init__(self, scheme=None, realm=None, use_ha1_pw=False) |
264 | 331 | |
309 | 376 | .. method:: get_password(password_callback) |
310 | 377 | |
311 | 378 | See basic authentication for documentation and examples. |
312 | ||
379 | ||
380 | .. method:: get_user_roles(roles_callback) | |
381 | ||
382 | See basic authentication for documentation and examples. | |
383 | ||
313 | 384 | .. method:: error_handler(error_callback) |
314 | 385 | |
315 | 386 | See basic authentication for documentation and examples. |
318 | 389 | |
319 | 390 | See basic authentication for documentation and examples. |
320 | 391 | |
392 | .. method:: current_user() | |
393 | ||
394 | See basic authentication for documentation and examples. | |
395 | ||
321 | 396 | .. method:: username() |
322 | 397 | |
323 | 398 | See basic authentication for documentation and examples. |
326 | 401 | |
327 | 402 | This class handles HTTP authentication with custom schemes for Flask routes. |
328 | 403 | |
329 | .. method:: __init__(scheme, realm=None) | |
404 | .. method:: __init__(scheme='Bearer', realm=None, header=None) | |
330 | 405 | |
331 | 406 | Create a token authentication object. |
332 | 407 | |
333 | The ``scheme`` argument must be provided to be used in the ``WWW-Authenticate`` response. | |
408 | The ``scheme`` argument can be use to specify the scheme to be used in the ``WWW-Authenticate`` response. The ``Authorization`` header sent by the client must include this scheme followed by the token. Example:: | |
409 | ||
410 | Authorization: Bearer this-is-my-token | |
334 | 411 | |
335 | 412 | The ``realm`` argument can be used to provide an application defined realm with the ``WWW-Authenticate`` header. |
336 | 413 | |
414 | The ``header`` argument can be used to specify a custom header instead of ``Authorization`` from where to obtain the token. If a custom header is used, the ``scheme`` should not be included. Example:: | |
415 | ||
416 | X-API-Key: this-is-my-token | |
417 | ||
337 | 418 | .. method:: verify_token(verify_token_callback) |
338 | 419 | |
339 | This callback function will be called by the framework to verify that the credentials sent by the client with the ``Authorization`` header are valid. The callback function takes one argument, the username and the password and must return ``True`` or ``False``. Example usage:: | |
420 | This callback function will be called by the framework to verify that the credentials sent by the client with the ``Authorization`` header are valid. The callback function takes one argument, the token provided by the client. The function must return the user object if the token is valid, or ``True`` if a user object is not available. In case of a failed authentication, the function should return ``None`` or ``False``. Example usage:: | |
340 | 421 | |
341 | 422 | @auth.verify_token |
342 | 423 | def verify_token(token): |
343 | g.current_user = User.query.filter_by(token=token).first() | |
344 | return g.current_user is not None | |
424 | return User.query.filter_by(token=token).first() | |
345 | 425 | |
346 | 426 | Note that a ``verify_token`` callback is required when using this class. |
347 | 427 | |
428 | .. method:: get_user_roles(roles_callback) | |
429 | ||
430 | See basic authentication for documentation and examples. | |
431 | ||
348 | 432 | .. method:: error_handler(error_callback) |
349 | 433 | |
350 | 434 | See basic authentication for documentation and examples. |
352 | 436 | .. method:: login_required(view_function_callback) |
353 | 437 | |
354 | 438 | See basic authentication for documentation and examples. |
439 | ||
440 | .. method:: current_user() | |
441 | ||
442 | See basic authentication for documentation and examples. | |
443 | ||
444 | .. class:: HTTPMultiAuth | |
445 | ||
446 | This class handles HTTP authentication with custom schemes for Flask routes. | |
447 | ||
448 | .. method:: __init__(auth_object, ...) | |
449 | ||
450 | Create a multiple authentication object. | |
451 | ||
452 | The arguments are one or more instances of ``HTTPBasicAuth``, ``HTTPDigestAuth`` or ``HTTPTokenAuth``. A route protected with this authentication method will try all the given authentication objects until one succeeds. | |
453 | ||
454 | .. method:: login_required(view_function_callback) | |
455 | ||
456 | See basic authentication for documentation and examples. | |
457 | ||
458 | .. method:: current_user() | |
459 | ||
460 | See basic authentication for documentation and examples. |
22 | 22 | |
23 | 23 | @auth.verify_password |
24 | 24 | def verify_password(username, password): |
25 | if username in users: | |
26 | return check_password_hash(users.get(username), password) | |
27 | return False | |
25 | if username in users and check_password_hash(users.get(username), | |
26 | password): | |
27 | return username | |
28 | 28 | |
29 | 29 | |
30 | 30 | @app.route('/') |
31 | 31 | @auth.login_required |
32 | 32 | def index(): |
33 | return "Hello, %s!" % auth.username() | |
33 | return "Hello, %s!" % auth.current_user() | |
34 | 34 | |
35 | 35 | |
36 | 36 | if __name__ == '__main__': |
4 | 4 | "MultiAuth" class. |
5 | 5 | |
6 | 6 | The root URL for this application can be accessed via basic auth, providing |
7 | username and password, or via token auth, providing a bearer JWT token. | |
7 | username and password, or via token auth, providing a bearer JWS token. | |
8 | 8 | """ |
9 | from flask import Flask, g | |
9 | from flask import Flask | |
10 | 10 | from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth, MultiAuth |
11 | 11 | from werkzeug.security import generate_password_hash, check_password_hash |
12 | from itsdangerous import TimedJSONWebSignatureSerializer as JWT | |
12 | from itsdangerous import TimedJSONWebSignatureSerializer as JWS | |
13 | 13 | |
14 | 14 | |
15 | 15 | app = Flask(__name__) |
16 | 16 | app.config['SECRET_KEY'] = 'top secret!' |
17 | jwt = JWT(app.config['SECRET_KEY'], expires_in=3600) | |
17 | jws = JWS(app.config['SECRET_KEY'], expires_in=3600) | |
18 | 18 | |
19 | 19 | basic_auth = HTTPBasicAuth() |
20 | 20 | token_auth = HTTPTokenAuth('Bearer') |
27 | 27 | } |
28 | 28 | |
29 | 29 | for user in users.keys(): |
30 | token = jwt.dumps({'username': user}) | |
30 | token = jws.dumps({'username': user}) | |
31 | 31 | print('*** token for {}: {}\n'.format(user, token)) |
32 | 32 | |
33 | 33 | |
34 | 34 | @basic_auth.verify_password |
35 | 35 | def verify_password(username, password): |
36 | g.user = None | |
37 | 36 | if username in users: |
38 | 37 | if check_password_hash(users.get(username), password): |
39 | g.user = username | |
40 | return True | |
41 | return False | |
38 | return username | |
42 | 39 | |
43 | 40 | |
44 | 41 | @token_auth.verify_token |
45 | 42 | def verify_token(token): |
46 | g.user = None | |
47 | 43 | try: |
48 | data = jwt.loads(token) | |
44 | data = jws.loads(token) | |
49 | 45 | except: # noqa: E722 |
50 | 46 | return False |
51 | 47 | if 'username' in data: |
52 | g.user = data['username'] | |
53 | return True | |
54 | return False | |
48 | return data['username'] | |
55 | 49 | |
56 | 50 | |
57 | 51 | @app.route('/') |
58 | 52 | @multi_auth.login_required |
59 | 53 | def index(): |
60 | return "Hello, %s!" % g.user | |
54 | return "Hello, %s!" % multi_auth.current_user() | |
61 | 55 | |
62 | 56 | |
63 | 57 | if __name__ == '__main__': |
0 | #!/usr/bin/env python | |
1 | """Basic authentication example | |
2 | ||
3 | This example demonstrates how to protect Flask endpoints with basic | |
4 | authentication, using secure hashed passwords. | |
5 | ||
6 | After running this example, visit http://localhost:5000 in your browser. To | |
7 | gain access, you can use (username=john, password=hello) or | |
8 | (username=susan, password=bye). | |
9 | """ | |
10 | from flask import Flask | |
11 | from flask_httpauth import HTTPBasicAuth | |
12 | from werkzeug.security import generate_password_hash, check_password_hash | |
13 | ||
14 | app = Flask(__name__) | |
15 | auth = HTTPBasicAuth() | |
16 | ||
17 | users = { | |
18 | "john": generate_password_hash("hello"), | |
19 | "susan": generate_password_hash("bye"), | |
20 | } | |
21 | ||
22 | roles = { | |
23 | "john": "user", | |
24 | "susan": ["user", "admin"], | |
25 | } | |
26 | ||
27 | ||
28 | @auth.get_user_roles | |
29 | def get_user_roles(username): | |
30 | return roles.get(username) | |
31 | ||
32 | ||
33 | @auth.verify_password | |
34 | def verify_password(username, password): | |
35 | if username in users and check_password_hash( | |
36 | users.get(username), password): | |
37 | return username | |
38 | ||
39 | ||
40 | @app.route('/') | |
41 | @auth.login_required(role='user') | |
42 | def index(): | |
43 | return "Hello, {}!".format(auth.current_user()) | |
44 | ||
45 | ||
46 | @app.route('/admin') | |
47 | @auth.login_required(role='admin') | |
48 | def admin(): | |
49 | return "Hello {}, you are an admin!".format(auth.current_user()) | |
50 | ||
51 | ||
52 | if __name__ == '__main__': | |
53 | app.run(debug=True, host='0.0.0.0') |
7 | 7 | To gain access, you can use a command line HTTP client such as curl, passing |
8 | 8 | one of the tokens: |
9 | 9 | |
10 | curl -X GET -H "Authorization: Bearer <jwt-token>" http://localhost:5000/ | |
10 | curl -X GET -H "Authorization: Bearer <jws-token>" http://localhost:5000/ | |
11 | 11 | |
12 | 12 | The response should include the username, which is obtained from the token. |
13 | 13 | """ |
14 | from flask import Flask, g | |
14 | from flask import Flask | |
15 | 15 | from flask_httpauth import HTTPTokenAuth |
16 | 16 | from itsdangerous import TimedJSONWebSignatureSerializer as Serializer |
17 | 17 | |
31 | 31 | |
32 | 32 | @auth.verify_token |
33 | 33 | def verify_token(token): |
34 | g.user = None | |
35 | 34 | try: |
36 | 35 | data = token_serializer.loads(token) |
37 | 36 | except: # noqa: E722 |
38 | 37 | return False |
39 | 38 | if 'username' in data: |
40 | g.user = data['username'] | |
41 | return True | |
42 | return False | |
39 | return data['username'] | |
43 | 40 | |
44 | 41 | |
45 | 42 | @app.route('/') |
46 | 43 | @auth.login_required |
47 | 44 | def index(): |
48 | return "Hello, %s!" % g.user | |
45 | return "Hello, %s!" % auth.current_user() | |
49 | 46 | |
50 | 47 | |
51 | 48 | if __name__ == '__main__': |
6 | 6 | :copyright: (C) 2014 by Miguel Grinberg. |
7 | 7 | :license: MIT, see LICENSE for more details. |
8 | 8 | """ |
9 | ||
9 | import hmac | |
10 | from base64 import b64decode | |
10 | 11 | from functools import wraps |
11 | 12 | from hashlib import md5 |
12 | 13 | from random import Random, SystemRandom |
13 | from flask import request, make_response, session | |
14 | from flask import request, make_response, session, g, Response | |
14 | 15 | from werkzeug.datastructures import Authorization |
15 | 16 | |
16 | __version__ = '3.2.4' | |
17 | ||
18 | __version__ = '4.4.0' | |
17 | 19 | |
18 | 20 | |
19 | 21 | class HTTPAuth(object): |
20 | def __init__(self, scheme=None, realm=None): | |
22 | def __init__(self, scheme=None, realm=None, header=None): | |
21 | 23 | self.scheme = scheme |
22 | 24 | self.realm = realm or "Authentication Required" |
25 | self.header = header | |
23 | 26 | self.get_password_callback = None |
27 | self.get_user_roles_callback = None | |
24 | 28 | self.auth_error_callback = None |
25 | 29 | |
26 | 30 | def default_get_password(username): |
27 | 31 | return None |
28 | 32 | |
29 | def default_auth_error(): | |
30 | return "Unauthorized Access" | |
33 | def default_auth_error(status): | |
34 | return "Unauthorized Access", status | |
31 | 35 | |
32 | 36 | self.get_password(default_get_password) |
33 | 37 | self.error_handler(default_auth_error) |
34 | 38 | |
39 | def is_compatible_auth(self, headers): | |
40 | if self.header is None or self.header == 'Authorization': | |
41 | try: | |
42 | scheme, _ = request.headers.get('Authorization', '').split( | |
43 | None, 1) | |
44 | except ValueError: | |
45 | # malformed Authorization header | |
46 | return False | |
47 | return scheme == self.scheme | |
48 | else: | |
49 | return self.header in headers | |
50 | ||
35 | 51 | def get_password(self, f): |
36 | 52 | self.get_password_callback = f |
53 | return f | |
54 | ||
55 | def get_user_roles(self, f): | |
56 | self.get_user_roles_callback = f | |
37 | 57 | return f |
38 | 58 | |
39 | 59 | def error_handler(self, f): |
40 | 60 | @wraps(f) |
41 | 61 | def decorated(*args, **kwargs): |
42 | 62 | res = f(*args, **kwargs) |
63 | check_status_code = not isinstance(res, (tuple, Response)) | |
43 | 64 | res = make_response(res) |
44 | if res.status_code == 200: | |
65 | if check_status_code and res.status_code == 200: | |
45 | 66 | # if user didn't set status code, use 401 |
46 | 67 | res.status_code = 401 |
47 | 68 | if 'WWW-Authenticate' not in res.headers.keys(): |
54 | 75 | return '{0} realm="{1}"'.format(self.scheme, self.realm) |
55 | 76 | |
56 | 77 | def get_auth(self): |
57 | auth = request.authorization | |
58 | if auth is None and 'Authorization' in request.headers: | |
59 | # Flask/Werkzeug do not recognize any authentication types | |
60 | # other than Basic or Digest, so here we parse the header by | |
61 | # hand | |
62 | try: | |
63 | auth_type, token = request.headers['Authorization'].split( | |
64 | None, 1) | |
65 | auth = Authorization(auth_type, {'token': token}) | |
66 | except ValueError: | |
67 | # The Authorization header is either empty or has no token | |
68 | pass | |
78 | auth = None | |
79 | if self.header is None or self.header == 'Authorization': | |
80 | auth = request.authorization | |
81 | if auth is None and 'Authorization' in request.headers: | |
82 | # Flask/Werkzeug do not recognize any authentication types | |
83 | # other than Basic or Digest, so here we parse the header by | |
84 | # hand | |
85 | try: | |
86 | auth_type, token = request.headers['Authorization'].split( | |
87 | None, 1) | |
88 | auth = Authorization(auth_type, {'token': token}) | |
89 | except (ValueError, KeyError): | |
90 | # The Authorization header is either empty or has no token | |
91 | pass | |
92 | elif self.header in request.headers: | |
93 | # using a custom header, so the entire value of the header is | |
94 | # assumed to be a token | |
95 | auth = Authorization(self.scheme, | |
96 | {'token': request.headers[self.header]}) | |
69 | 97 | |
70 | 98 | # if the auth type does not match, we act as if there is no auth |
71 | 99 | # this is better than failing directly, as it allows the callback |
83 | 111 | |
84 | 112 | return password |
85 | 113 | |
86 | def login_required(self, f): | |
87 | @wraps(f) | |
88 | def decorated(*args, **kwargs): | |
89 | auth = self.get_auth() | |
90 | ||
91 | # Flask normally handles OPTIONS requests on its own, but in the | |
92 | # case it is configured to forward those to the application, we | |
93 | # need to ignore authentication headers and let the request through | |
94 | # to avoid unwanted interactions with CORS. | |
95 | if request.method != 'OPTIONS': # pragma: no cover | |
96 | password = self.get_auth_password(auth) | |
97 | ||
98 | if not self.authenticate(auth, password): | |
99 | # Clear TCP receive buffer of any pending data | |
100 | request.data | |
101 | return self.auth_error_callback() | |
102 | ||
103 | return f(*args, **kwargs) | |
104 | return decorated | |
114 | def authorize(self, role, user, auth): | |
115 | if role is None: | |
116 | return True | |
117 | if isinstance(role, (list, tuple)): | |
118 | roles = role | |
119 | else: | |
120 | roles = [role] | |
121 | if user is True: | |
122 | user = auth | |
123 | if self.get_user_roles_callback is None: # pragma: no cover | |
124 | raise ValueError('get_user_roles callback is not defined') | |
125 | user_roles = self.get_user_roles_callback(user) | |
126 | if user_roles is None: | |
127 | user_roles = {} | |
128 | elif not isinstance(user_roles, (list, tuple)): | |
129 | user_roles = {user_roles} | |
130 | else: | |
131 | user_roles = set(user_roles) | |
132 | for role in roles: | |
133 | if isinstance(role, (list, tuple)): | |
134 | role = set(role) | |
135 | if role & user_roles == role: | |
136 | return True | |
137 | elif role in user_roles: | |
138 | return True | |
139 | ||
140 | def login_required(self, f=None, role=None, optional=None): | |
141 | if f is not None and \ | |
142 | (role is not None or optional is not None): # pragma: no cover | |
143 | raise ValueError( | |
144 | 'role and optional are the only supported arguments') | |
145 | ||
146 | def login_required_internal(f): | |
147 | @wraps(f) | |
148 | def decorated(*args, **kwargs): | |
149 | auth = self.get_auth() | |
150 | ||
151 | # Flask normally handles OPTIONS requests on its own, but in | |
152 | # the case it is configured to forward those to the | |
153 | # application, we need to ignore authentication headers and | |
154 | # let the request through to avoid unwanted interactions with | |
155 | # CORS. | |
156 | if request.method != 'OPTIONS': # pragma: no cover | |
157 | password = self.get_auth_password(auth) | |
158 | ||
159 | status = None | |
160 | user = self.authenticate(auth, password) | |
161 | if user in (False, None): | |
162 | status = 401 | |
163 | elif not self.authorize(role, user, auth): | |
164 | status = 403 | |
165 | if not optional and status: | |
166 | # Clear TCP receive buffer of any pending data | |
167 | request.data | |
168 | try: | |
169 | return self.auth_error_callback(status) | |
170 | except TypeError: | |
171 | return self.auth_error_callback() | |
172 | ||
173 | g.flask_httpauth_user = user if user is not True \ | |
174 | else auth.username if auth else None | |
175 | return f(*args, **kwargs) | |
176 | return decorated | |
177 | ||
178 | if f: | |
179 | return login_required_internal(f) | |
180 | return login_required_internal | |
105 | 181 | |
106 | 182 | def username(self): |
107 | if not request.authorization: | |
183 | auth = self.get_auth() | |
184 | if not auth: | |
108 | 185 | return "" |
109 | return request.authorization.username | |
186 | return auth.username | |
187 | ||
188 | def current_user(self): | |
189 | if hasattr(g, 'flask_httpauth_user'): | |
190 | return g.flask_httpauth_user | |
110 | 191 | |
111 | 192 | |
112 | 193 | class HTTPBasicAuth(HTTPAuth): |
123 | 204 | def verify_password(self, f): |
124 | 205 | self.verify_password_callback = f |
125 | 206 | return f |
207 | ||
208 | def get_auth(self): | |
209 | # this version of the Authorization header parser is more flexible | |
210 | # than Werkzeug's, as it also accepts other schemes besides "Basic" | |
211 | header = self.header or 'Authorization' | |
212 | if header not in request.headers: | |
213 | return None | |
214 | value = request.headers[header].encode('utf-8') | |
215 | try: | |
216 | scheme, credentials = value.split(b' ', 1) | |
217 | username, password = b64decode(credentials).split(b':', 1) | |
218 | except (ValueError, TypeError): | |
219 | return None | |
220 | try: | |
221 | username = username.decode('utf-8') | |
222 | password = password.decode('utf-8') | |
223 | except UnicodeDecodeError: | |
224 | username = None | |
225 | password = None | |
226 | return Authorization( | |
227 | scheme, {'username': username, 'password': password}) | |
126 | 228 | |
127 | 229 | def authenticate(self, auth, stored_password): |
128 | 230 | if auth: |
134 | 236 | if self.verify_password_callback: |
135 | 237 | return self.verify_password_callback(username, client_password) |
136 | 238 | if not auth: |
137 | return False | |
239 | return | |
138 | 240 | if self.hash_password_callback: |
139 | 241 | try: |
140 | 242 | client_password = self.hash_password_callback(client_password) |
141 | 243 | except TypeError: |
142 | 244 | client_password = self.hash_password_callback(username, |
143 | 245 | client_password) |
144 | return client_password is not None and \ | |
145 | client_password == stored_password | |
246 | return auth.username if client_password is not None and \ | |
247 | stored_password is not None and \ | |
248 | hmac.compare_digest(client_password, stored_password) else None | |
146 | 249 | |
147 | 250 | |
148 | 251 | class HTTPDigestAuth(HTTPAuth): |
168 | 271 | return session["auth_nonce"] |
169 | 272 | |
170 | 273 | def default_verify_nonce(nonce): |
171 | return nonce == session.get("auth_nonce") | |
274 | session_nonce = session.get("auth_nonce") | |
275 | if nonce is None or session_nonce is None: | |
276 | return False | |
277 | return hmac.compare_digest(nonce, session_nonce) | |
172 | 278 | |
173 | 279 | def default_generate_opaque(): |
174 | 280 | session["auth_opaque"] = _generate_random() |
175 | 281 | return session["auth_opaque"] |
176 | 282 | |
177 | 283 | def default_verify_opaque(opaque): |
178 | return opaque == session.get("auth_opaque") | |
284 | session_opaque = session.get("auth_opaque") | |
285 | if opaque is None or session_opaque is None: # pragma: no cover | |
286 | return False | |
287 | return hmac.compare_digest(opaque, session_opaque) | |
179 | 288 | |
180 | 289 | self.generate_nonce(default_generate_nonce) |
181 | 290 | self.generate_opaque(default_generate_opaque) |
234 | 343 | ha2 = md5(a2.encode('utf-8')).hexdigest() |
235 | 344 | a3 = ha1 + ":" + auth.nonce + ":" + ha2 |
236 | 345 | response = md5(a3.encode('utf-8')).hexdigest() |
237 | return response == auth.response | |
346 | return hmac.compare_digest(response, auth.response) | |
238 | 347 | |
239 | 348 | |
240 | 349 | class HTTPTokenAuth(HTTPAuth): |
241 | def __init__(self, scheme='Bearer', realm=None): | |
242 | super(HTTPTokenAuth, self).__init__(scheme, realm) | |
350 | def __init__(self, scheme='Bearer', realm=None, header=None): | |
351 | super(HTTPTokenAuth, self).__init__(scheme, realm, header) | |
243 | 352 | |
244 | 353 | self.verify_token_callback = None |
245 | 354 | |
254 | 363 | token = "" |
255 | 364 | if self.verify_token_callback: |
256 | 365 | return self.verify_token_callback(token) |
257 | return False | |
258 | 366 | |
259 | 367 | |
260 | 368 | class MultiAuth(object): |
262 | 370 | self.main_auth = main_auth |
263 | 371 | self.additional_auth = args |
264 | 372 | |
265 | def login_required(self, f): | |
266 | @wraps(f) | |
267 | def decorated(*args, **kwargs): | |
268 | selected_auth = None | |
269 | if 'Authorization' in request.headers: | |
270 | try: | |
271 | scheme, creds = request.headers['Authorization'].split( | |
272 | None, 1) | |
273 | except ValueError: | |
274 | # malformed Authorization header | |
275 | pass | |
276 | else: | |
373 | def login_required(self, f=None, role=None, optional=None): | |
374 | if f is not None and \ | |
375 | (role is not None or optional is not None): # pragma: no cover | |
376 | raise ValueError( | |
377 | 'role and optional are the only supported arguments') | |
378 | ||
379 | def login_required_internal(f): | |
380 | @wraps(f) | |
381 | def decorated(*args, **kwargs): | |
382 | selected_auth = self.main_auth | |
383 | if not self.main_auth.is_compatible_auth(request.headers): | |
277 | 384 | for auth in self.additional_auth: |
278 | if auth.scheme == scheme: | |
385 | if auth.is_compatible_auth(request.headers): | |
279 | 386 | selected_auth = auth |
280 | 387 | break |
281 | if selected_auth is None: | |
282 | selected_auth = self.main_auth | |
283 | return selected_auth.login_required(f)(*args, **kwargs) | |
284 | return decorated | |
388 | return selected_auth.login_required(role=role, | |
389 | optional=optional | |
390 | )(f)(*args, **kwargs) | |
391 | return decorated | |
392 | ||
393 | if f: | |
394 | return login_required_internal(f) | |
395 | return login_required_internal | |
396 | ||
397 | def current_user(self): | |
398 | if hasattr(g, 'flask_httpauth_user'): # pragma: no cover | |
399 | return g.flask_httpauth_user |
33 | 33 | 'License :: OSI Approved :: MIT License', |
34 | 34 | 'Operating System :: OS Independent', |
35 | 35 | 'Programming Language :: Python', |
36 | 'Programming Language :: Python :: 2', | |
37 | 36 | 'Programming Language :: Python :: 3', |
38 | 37 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', |
39 | 38 | 'Topic :: Software Development :: Libraries :: Python Modules' |
4 | 4 | |
5 | 5 | |
6 | 6 | class HTTPAuthTestCase(unittest.TestCase): |
7 | use_old_style_callback = False | |
8 | ||
7 | 9 | def setUp(self): |
8 | 10 | app = Flask(__name__) |
9 | 11 | app.config['SECRET_KEY'] = 'my secret' |
12 | 14 | |
13 | 15 | @basic_verify_auth.verify_password |
14 | 16 | def basic_verify_auth_verify_password(username, password): |
15 | g.anon = False | |
16 | if username == 'john': | |
17 | return password == 'hello' | |
18 | elif username == 'susan': | |
19 | return password == 'bye' | |
20 | elif username == '': | |
21 | g.anon = True | |
22 | return True | |
23 | return False | |
17 | if self.use_old_style_callback: | |
18 | g.anon = False | |
19 | if username == 'john': | |
20 | return password == 'hello' | |
21 | elif username == 'susan': | |
22 | return password == 'bye' | |
23 | elif username == '': | |
24 | g.anon = True | |
25 | return True | |
26 | return False | |
27 | else: | |
28 | g.anon = False | |
29 | if username == 'john' and password == 'hello': | |
30 | return 'john' | |
31 | elif username == 'susan' and password == 'bye': | |
32 | return 'susan' | |
33 | elif username == '': | |
34 | g.anon = True | |
35 | return '' | |
24 | 36 | |
25 | 37 | @basic_verify_auth.error_handler |
26 | 38 | def error_handler(): |
39 | self.assertIsNone(basic_verify_auth.current_user()) | |
27 | 40 | return 'error', 403 # use a custom error status |
28 | 41 | |
29 | 42 | @app.route('/') |
33 | 46 | @app.route('/basic-verify') |
34 | 47 | @basic_verify_auth.login_required |
35 | 48 | def basic_verify_auth_route(): |
36 | return 'basic_verify_auth:' + basic_verify_auth.username() + \ | |
37 | ' anon:' + str(g.anon) | |
49 | if self.use_old_style_callback: | |
50 | return 'basic_verify_auth:' + basic_verify_auth.username() + \ | |
51 | ' anon:' + str(g.anon) | |
52 | else: | |
53 | return 'basic_verify_auth:' + \ | |
54 | basic_verify_auth.current_user() + ' anon:' + str(g.anon) | |
38 | 55 | |
39 | 56 | self.app = app |
40 | 57 | self.basic_verify_auth = basic_verify_auth |
56 | 73 | '/basic-verify', headers={'Authorization': 'Basic ' + creds}) |
57 | 74 | self.assertEqual(response.status_code, 403) |
58 | 75 | self.assertTrue('WWW-Authenticate' in response.headers) |
76 | ||
77 | def test_verify_auth_login_malformed_password(self): | |
78 | creds = 'eyJhbGciOieyJp==' | |
79 | response = self.client.get('/basic-verify', | |
80 | headers={'Authorization': 'Basic ' + creds}) | |
81 | self.assertEqual(response.status_code, 403) | |
82 | self.assertTrue('WWW-Authenticate' in response.headers) | |
83 | ||
84 | ||
85 | class HTTPAuthTestCaseOldStyle(HTTPAuthTestCase): | |
86 | use_old_style_callback = True |
0 | import unittest | |
1 | import base64 | |
2 | from flask import Flask, Response | |
3 | from flask_httpauth import HTTPBasicAuth | |
4 | ||
5 | ||
6 | class HTTPAuthTestCase(unittest.TestCase): | |
7 | responses = [ | |
8 | ['error', 401], | |
9 | [('error', 403), 403], | |
10 | [('error', 200), 200], | |
11 | [Response('error'), 200], | |
12 | [Response('error', 403), 403], | |
13 | ] | |
14 | ||
15 | def setUp(self): | |
16 | app = Flask(__name__) | |
17 | app.config['SECRET_KEY'] = 'my secret' | |
18 | ||
19 | basic_verify_auth = HTTPBasicAuth() | |
20 | ||
21 | @basic_verify_auth.verify_password | |
22 | def basic_verify_auth_verify_password(username, password): | |
23 | return False | |
24 | ||
25 | @basic_verify_auth.error_handler | |
26 | def error_handler(): | |
27 | self.assertIsNone(basic_verify_auth.current_user()) | |
28 | return self.error_response | |
29 | ||
30 | @app.route('/') | |
31 | @basic_verify_auth.login_required | |
32 | def index(): | |
33 | return 'index' | |
34 | ||
35 | self.app = app | |
36 | self.basic_verify_auth = basic_verify_auth | |
37 | self.client = app.test_client() | |
38 | ||
39 | def test_default_status_code(self): | |
40 | creds = base64.b64encode(b'foo:bar').decode('utf-8') | |
41 | ||
42 | for r in self.responses: | |
43 | self.error_response = r[0] | |
44 | response = self.client.get( | |
45 | '/', headers={'Authorization': 'Basic ' + creds}) | |
46 | self.assertEqual(response.status_code, r[1]) |
10 | 10 | |
11 | 11 | basic_auth = HTTPBasicAuth() |
12 | 12 | token_auth = HTTPTokenAuth('MyToken') |
13 | multi_auth = MultiAuth(basic_auth, token_auth) | |
13 | custom_token_auth = HTTPTokenAuth(header='X-Token') | |
14 | multi_auth = MultiAuth(basic_auth, token_auth, custom_token_auth) | |
14 | 15 | |
15 | 16 | @basic_auth.verify_password |
16 | 17 | def verify_password(username, password): |
17 | return username == 'john' and password == 'hello' | |
18 | if username == 'john' and password == 'hello': | |
19 | return 'john' | |
20 | ||
21 | @basic_auth.get_user_roles | |
22 | def get_basic_role(username): | |
23 | if username == 'john': | |
24 | return ['foo', 'bar'] | |
18 | 25 | |
19 | 26 | @token_auth.verify_token |
20 | 27 | def verify_token(token): |
21 | 28 | return token == 'this-is-the-token!' |
22 | 29 | |
30 | @token_auth.get_user_roles | |
31 | def get_token_role(auth): | |
32 | if auth['token'] == 'this-is-the-token!': | |
33 | return 'foo' | |
34 | return | |
35 | ||
23 | 36 | @token_auth.error_handler |
24 | 37 | def error_handler(): |
25 | 38 | return 'error', 401, {'WWW-Authenticate': 'MyToken realm="Foo"'} |
39 | ||
40 | @custom_token_auth.verify_token | |
41 | def verify_custom_token(token): | |
42 | return token == 'this-is-the-custom-token!' | |
43 | ||
44 | @custom_token_auth.get_user_roles | |
45 | def get_custom_token_role(auth): | |
46 | if auth['token'] == 'this-is-the-custom-token!': | |
47 | return 'foo' | |
48 | return | |
26 | 49 | |
27 | 50 | @app.route('/') |
28 | 51 | def index(): |
31 | 54 | @app.route('/protected') |
32 | 55 | @multi_auth.login_required |
33 | 56 | def auth_route(): |
34 | return 'access granted' | |
57 | return 'access granted:' + str(multi_auth.current_user()) | |
58 | ||
59 | @app.route('/protected-with-role') | |
60 | @multi_auth.login_required(role='foo') | |
61 | def auth_role_route(): | |
62 | return 'role access granted' | |
35 | 63 | |
36 | 64 | self.app = app |
37 | 65 | self.client = app.test_client() |
47 | 75 | creds = base64.b64encode(b'john:hello').decode('utf-8') |
48 | 76 | response = self.client.get( |
49 | 77 | '/protected', headers={'Authorization': 'Basic ' + creds}) |
50 | self.assertEqual(response.data.decode('utf-8'), 'access granted') | |
78 | self.assertEqual(response.data.decode('utf-8'), 'access granted:john') | |
51 | 79 | |
52 | 80 | def test_multi_auth_login_invalid_basic(self): |
53 | 81 | creds = base64.b64encode(b'john:bye').decode('utf-8') |
62 | 90 | response = self.client.get( |
63 | 91 | '/protected', headers={'Authorization': |
64 | 92 | 'MyToken this-is-the-token!'}) |
65 | self.assertEqual(response.data.decode('utf-8'), 'access granted') | |
93 | self.assertEqual(response.data.decode('utf-8'), 'access granted:None') | |
66 | 94 | |
67 | 95 | def test_multi_auth_login_invalid_token(self): |
68 | 96 | response = self.client.get( |
72 | 100 | self.assertTrue('WWW-Authenticate' in response.headers) |
73 | 101 | self.assertEqual(response.headers['WWW-Authenticate'], |
74 | 102 | 'MyToken realm="Foo"') |
103 | ||
104 | def test_multi_auth_login_valid_custom_token(self): | |
105 | response = self.client.get( | |
106 | '/protected', headers={'X-Token': 'this-is-the-custom-token!'}) | |
107 | self.assertEqual(response.data.decode('utf-8'), 'access granted:None') | |
108 | ||
109 | def test_multi_auth_login_invalid_custom_token(self): | |
110 | response = self.client.get( | |
111 | '/protected', headers={'X-Token': 'this-is-not-the-token!'}) | |
112 | self.assertEqual(response.status_code, 401) | |
113 | self.assertTrue('WWW-Authenticate' in response.headers) | |
114 | self.assertEqual(response.headers['WWW-Authenticate'], | |
115 | 'Bearer realm="Authentication Required"') | |
75 | 116 | |
76 | 117 | def test_multi_auth_login_invalid_scheme(self): |
77 | 118 | response = self.client.get( |
85 | 126 | response = self.client.get( |
86 | 127 | '/protected', headers={'Authorization': 'token-without-scheme'}) |
87 | 128 | self.assertEqual(response.status_code, 401) |
129 | ||
130 | def test_multi_auth_login_valid_basic_role(self): | |
131 | creds = base64.b64encode(b'john:hello').decode('utf-8') | |
132 | response = self.client.get( | |
133 | '/protected-with-role', headers={'Authorization': | |
134 | 'Basic ' + creds}) | |
135 | self.assertEqual(response.data.decode('utf-8'), 'role access granted') | |
136 | ||
137 | def test_multi_auth_login_valid_token_role(self): | |
138 | response = self.client.get( | |
139 | '/protected-with-role', headers={'Authorization': | |
140 | 'MyToken this-is-the-token!'}) | |
141 | self.assertEqual(response.data.decode('utf-8'), 'role access granted') | |
142 | ||
143 | def test_multi_auth_login_valid_custom_token_role(self): | |
144 | response = self.client.get( | |
145 | '/protected-with-role', headers={'X-Token': | |
146 | 'this-is-the-custom-token!'}) | |
147 | self.assertEqual(response.data.decode('utf-8'), 'role access granted') |
0 | import unittest | |
1 | import base64 | |
2 | from flask import Flask, g | |
3 | from flask_httpauth import HTTPBasicAuth | |
4 | ||
5 | ||
6 | class HTTPAuthTestCase(unittest.TestCase): | |
7 | def setUp(self): | |
8 | app = Flask(__name__) | |
9 | app.config['SECRET_KEY'] = 'my secret' | |
10 | ||
11 | roles_auth = HTTPBasicAuth() | |
12 | ||
13 | @roles_auth.verify_password | |
14 | def roles_auth_verify_password(username, password): | |
15 | g.anon = False | |
16 | if username == 'john': | |
17 | return password == 'hello' | |
18 | elif username == 'susan': | |
19 | return password == 'bye' | |
20 | elif username == 'cindy': | |
21 | return password == 'byebye' | |
22 | elif username == '': | |
23 | g.anon = True | |
24 | return True | |
25 | return False | |
26 | ||
27 | @roles_auth.get_user_roles | |
28 | def get_user_roles(auth): | |
29 | username = auth.username | |
30 | if username == 'john': | |
31 | return 'normal' | |
32 | elif username == 'susan': | |
33 | return ('normal', 'special') | |
34 | elif username == 'cindy': | |
35 | return None | |
36 | ||
37 | @roles_auth.error_handler | |
38 | def error_handler(): | |
39 | return 'error', 403 # use a custom error status | |
40 | ||
41 | @app.route('/') | |
42 | def index(): | |
43 | return 'index' | |
44 | ||
45 | @app.route('/normal') | |
46 | @roles_auth.login_required(role='normal') | |
47 | def roles_auth_route_normal(): | |
48 | return 'normal:' + roles_auth.username() | |
49 | ||
50 | @app.route('/special') | |
51 | @roles_auth.login_required(role='special') | |
52 | def roles_auth_route_special(): | |
53 | return 'special:' + roles_auth.username() | |
54 | ||
55 | @app.route('/normal-or-special') | |
56 | @roles_auth.login_required(role=('normal', 'special')) | |
57 | def roles_auth_route_normal_or_special(): | |
58 | return 'normal_or_special:' + roles_auth.username() | |
59 | ||
60 | @app.route('/normal-and-special') | |
61 | @roles_auth.login_required(role=(('normal', 'special'),)) | |
62 | def roles_auth_route_normal_and_special(): | |
63 | return 'normal_and_special:' + roles_auth.username() | |
64 | ||
65 | self.app = app | |
66 | self.roles_auth = roles_auth | |
67 | self.client = app.test_client() | |
68 | ||
69 | def test_verify_roles_valid_normal_1(self): | |
70 | creds = base64.b64encode(b'susan:bye').decode('utf-8') | |
71 | response = self.client.get( | |
72 | '/normal', headers={'Authorization': 'Basic ' + creds}) | |
73 | self.assertEqual(response.data, b'normal:susan') | |
74 | ||
75 | def test_verify_roles_valid_normal_2(self): | |
76 | creds = base64.b64encode(b'john:hello').decode('utf-8') | |
77 | response = self.client.get( | |
78 | '/normal', headers={'Authorization': 'Basic ' + creds}) | |
79 | self.assertEqual(response.data, b'normal:john') | |
80 | ||
81 | def test_verify_auth_login_valid_special(self): | |
82 | creds = base64.b64encode(b'susan:bye').decode('utf-8') | |
83 | response = self.client.get( | |
84 | '/special', headers={'Authorization': 'Basic ' + creds}) | |
85 | self.assertEqual(response.data, b'special:susan') | |
86 | ||
87 | def test_verify_auth_login_invalid_special_1(self): | |
88 | creds = base64.b64encode(b'john:hello').decode('utf-8') | |
89 | response = self.client.get( | |
90 | '/special', headers={'Authorization': 'Basic ' + creds}) | |
91 | self.assertEqual(response.status_code, 403) | |
92 | self.assertTrue('WWW-Authenticate' in response.headers) | |
93 | ||
94 | def test_verify_auth_login_invalid_special_2(self): | |
95 | creds = base64.b64encode(b'cindy:byebye').decode('utf-8') | |
96 | response = self.client.get( | |
97 | '/special', headers={'Authorization': 'Basic ' + creds}) | |
98 | self.assertEqual(response.status_code, 403) | |
99 | self.assertTrue('WWW-Authenticate' in response.headers) | |
100 | ||
101 | def test_verify_auth_login_valid_normal_or_special_1(self): | |
102 | creds = base64.b64encode(b'susan:bye').decode('utf-8') | |
103 | response = self.client.get( | |
104 | '/normal-or-special', headers={'Authorization': 'Basic ' + creds}) | |
105 | self.assertEqual(response.data, b'normal_or_special:susan') | |
106 | ||
107 | def test_verify_auth_login_valid_normal_or_special_2(self): | |
108 | creds = base64.b64encode(b'john:hello').decode('utf-8') | |
109 | response = self.client.get( | |
110 | '/normal-or-special', headers={'Authorization': 'Basic ' + creds}) | |
111 | self.assertEqual(response.data, b'normal_or_special:john') | |
112 | ||
113 | def test_verify_auth_login_valid_normal_and_special_1(self): | |
114 | creds = base64.b64encode(b'susan:bye').decode('utf-8') | |
115 | response = self.client.get( | |
116 | '/normal-and-special', headers={'Authorization': 'Basic ' + creds}) | |
117 | self.assertEqual(response.data, b'normal_and_special:susan') | |
118 | ||
119 | def test_verify_auth_login_valid_normal_and_special_2(self): | |
120 | creds = base64.b64encode(b'john:hello').decode('utf-8') | |
121 | response = self.client.get( | |
122 | '/normal-and-special', headers={'Authorization': 'Basic ' + creds}) | |
123 | self.assertEqual(response.status_code, 403) | |
124 | self.assertTrue('WWW-Authenticate' in response.headers) | |
125 | ||
126 | def test_verify_auth_login_invalid_password(self): | |
127 | creds = base64.b64encode(b'john:bye').decode('utf-8') | |
128 | response = self.client.get( | |
129 | '/normal', headers={'Authorization': 'Basic ' + creds}) | |
130 | self.assertEqual(response.status_code, 403) | |
131 | self.assertTrue('WWW-Authenticate' in response.headers) |
0 | import base64 | |
0 | 1 | import unittest |
1 | 2 | from flask import Flask |
2 | 3 | from flask_httpauth import HTTPTokenAuth |
8 | 9 | app.config['SECRET_KEY'] = 'my secret' |
9 | 10 | |
10 | 11 | token_auth = HTTPTokenAuth('MyToken') |
12 | token_auth2 = HTTPTokenAuth('Token', realm='foo') | |
13 | token_auth3 = HTTPTokenAuth(header='X-API-Key') | |
11 | 14 | |
12 | 15 | @token_auth.verify_token |
13 | 16 | def verify_token(token): |
14 | return token == 'this-is-the-token!' | |
17 | if token == 'this-is-the-token!': | |
18 | return 'user' | |
19 | ||
20 | @token_auth3.verify_token | |
21 | def verify_token3(token): | |
22 | if token == 'this-is-the-token!': | |
23 | return 'user' | |
15 | 24 | |
16 | 25 | @token_auth.error_handler |
17 | 26 | def error_handler(): |
24 | 33 | @app.route('/protected') |
25 | 34 | @token_auth.login_required |
26 | 35 | def token_auth_route(): |
27 | return 'token_auth' | |
36 | return 'token_auth:' + token_auth.current_user() | |
37 | ||
38 | @app.route('/protected-optional') | |
39 | @token_auth.login_required(optional=True) | |
40 | def token_auth_optional_route(): | |
41 | return 'token_auth:' + str(token_auth.current_user()) | |
42 | ||
43 | @app.route('/protected2') | |
44 | @token_auth2.login_required | |
45 | def token_auth_route2(): | |
46 | return 'token_auth2' | |
47 | ||
48 | @app.route('/protected3') | |
49 | @token_auth3.login_required | |
50 | def token_auth_route3(): | |
51 | return 'token_auth3:' + token_auth3.current_user() | |
28 | 52 | |
29 | 53 | self.app = app |
30 | 54 | self.token_auth = token_auth |
46 | 70 | response = self.client.get( |
47 | 71 | '/protected', headers={'Authorization': |
48 | 72 | 'MyToken this-is-the-token!'}) |
49 | self.assertEqual(response.data.decode('utf-8'), 'token_auth') | |
73 | self.assertEqual(response.data.decode('utf-8'), 'token_auth:user') | |
50 | 74 | |
51 | 75 | def test_token_auth_login_valid_different_case(self): |
52 | 76 | response = self.client.get( |
53 | 77 | '/protected', headers={'Authorization': |
54 | 78 | 'mytoken this-is-the-token!'}) |
55 | self.assertEqual(response.data.decode('utf-8'), 'token_auth') | |
79 | self.assertEqual(response.data.decode('utf-8'), 'token_auth:user') | |
80 | ||
81 | def test_token_auth_login_optional(self): | |
82 | response = self.client.get('/protected-optional') | |
83 | self.assertEqual(response.data.decode('utf-8'), 'token_auth:None') | |
56 | 84 | |
57 | 85 | def test_token_auth_login_invalid_token(self): |
58 | 86 | response = self.client.get( |
80 | 108 | 'MyToken realm="Foo"') |
81 | 109 | |
82 | 110 | def test_token_auth_login_invalid_no_callback(self): |
83 | token_auth2 = HTTPTokenAuth('Token', realm='foo') | |
84 | ||
85 | @self.app.route('/protected2') | |
86 | @token_auth2.login_required | |
87 | def token_auth_route2(): | |
88 | return 'token_auth2' | |
89 | ||
90 | 111 | response = self.client.get( |
91 | 112 | '/protected2', headers={'Authorization': |
92 | 113 | 'Token this-is-the-token!'}) |
94 | 115 | self.assertTrue('WWW-Authenticate' in response.headers) |
95 | 116 | self.assertEqual(response.headers['WWW-Authenticate'], |
96 | 117 | 'Token realm="foo"') |
118 | ||
119 | def test_token_auth_custom_header_valid_token(self): | |
120 | response = self.client.get( | |
121 | '/protected3', headers={'X-API-Key': 'this-is-the-token!'}) | |
122 | self.assertEqual(response.status_code, 200) | |
123 | self.assertEqual(response.data.decode('utf-8'), 'token_auth3:user') | |
124 | ||
125 | def test_token_auth_custom_header_invalid_token(self): | |
126 | response = self.client.get( | |
127 | '/protected3', headers={'X-API-Key': 'invalid-token-should-fail'}) | |
128 | self.assertEqual(response.status_code, 401) | |
129 | self.assertTrue('WWW-Authenticate' in response.headers) | |
130 | ||
131 | def test_token_auth_custom_header_invalid_header(self): | |
132 | response = self.client.get( | |
133 | '/protected3', headers={'API-Key': 'this-is-the-token!'}) | |
134 | self.assertEqual(response.status_code, 401) | |
135 | self.assertTrue('WWW-Authenticate' in response.headers) | |
136 | self.assertEqual(response.headers['WWW-Authenticate'], | |
137 | 'Bearer realm="Authentication Required"') | |
138 | ||
139 | def test_token_auth_header_precedence(self): | |
140 | basic_creds = base64.b64encode(b'susan:bye').decode('utf-8') | |
141 | response = self.client.get( | |
142 | '/protected3', headers={'Authorization': 'Basic ' + basic_creds, | |
143 | 'X-API-Key': 'this-is-the-token!'}) | |
144 | self.assertEqual(response.status_code, 200) | |
145 | self.assertEqual(response.data.decode('utf-8'), 'token_auth3:user') |
0 | 0 | [tox] |
1 | envlist=flake8,py27,py34,py35,py36,pypy,docs,coverage | |
1 | envlist=flake8,py36,py37,py38,py39,pypy3,docs,coverage | |
2 | 2 | skip_missing_interpreters=True |
3 | ||
4 | [gh-actions] | |
5 | python = | |
6 | 2.7: py27 | |
7 | 3.6: py36 | |
8 | 3.7: py37 | |
9 | 3.8: py38 | |
10 | 3.9: py39 | |
11 | pypy2: pypy2 | |
12 | pypy3: pypy3 | |
3 | 13 | |
4 | 14 | [testenv] |
5 | 15 | commands= |
6 | 16 | coverage run --branch --include=flask_httpauth.py setup.py test |
7 | 17 | coverage report --show-missing |
18 | coverage xml -o coverage.xml | |
8 | 19 | coverage erase |
9 | 20 | deps= |
10 | 21 | coverage |
11 | 22 | |
12 | 23 | [testenv:flake8] |
13 | basepython=python | |
14 | 24 | deps= |
15 | 25 | flake8 |
16 | 26 | commands= |
17 | 27 | flake8 --exclude=".*" --ignore=E402 flask_httpauth.py tests examples |
18 | 28 | |
19 | [testenv:py26] | |
20 | basepython=python2.6 | |
21 | ||
22 | [testenv:py27] | |
23 | basepython=python2.7 | |
24 | ||
25 | [testenv:py34] | |
26 | basepython=python3.4 | |
27 | ||
28 | [testenv:py35] | |
29 | basepython=python3.5 | |
30 | ||
31 | [testenv:py36] | |
32 | basepython=python3.6 | |
33 | ||
34 | [testenv:pypy] | |
35 | basepython=pypy | |
36 | ||
37 | 29 | [testenv:docs] |
38 | basepython=python2.7 | |
39 | 30 | changedir=docs |
40 | 31 | deps= |
41 | 32 | sphinx |
43 | 34 | make |
44 | 35 | commands= |
45 | 36 | make html |
46 | ||
47 | [testenv:coverage] | |
48 | basepython=python | |
49 | commands= | |
50 | coverage run --branch --source=flask_httpauth.py setup.py test | |
51 | coverage html | |
52 | coverage erase |