Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add a way to configure basic auth without storing passwords in plaintext in settings #2

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

immerrr
Copy link

@immerrr immerrr commented Jul 11, 2021

This PR's goal is to enable storing password digests (instead of plaintext) to increase security.

It is admittedly a very early version aimed mostly at collecting feedback. I tried to introduce this with as little change as possible to the existing functionality to maintain backward compat, althought it is probably possible to add this to the basic_auth class directly.

I'm open to suggestions, and I have enabled edits by maintainers if you feel like applying some minor changes directly. Also, feel free to take this as a proof-of-concept, and implement it in a completely independent branch, that's absolutely fine by me.

@immerrr
Copy link
Author

immerrr commented Oct 27, 2021

@jmagnusson is there anything I can do to help get the ball rolling here?

@jacobsvante
Copy link
Owner

Hi @immerrr,

What's your use case for saving the passwords as base64 digests? Really, base64 is just an obfuscation method and shouldn't be seen as a way to increase security.

I would recommend that you take a look at sops, which can be used to encrypt the data on its way to the server (i.e. CI/CD).

@immerrr
Copy link
Author

immerrr commented Oct 29, 2021

@jmagnusson

please note, that in the PR

https://github.com/jmagnusson/fastapi-security/pull/2/files#diff-be84d30c53432e00c4d6f032086d71535047f3cdac687bb239a8da07f15e9d63R95-R99

before base64 is applied to the digest, it is actually hashed with SHA512 which should have the necessary protection from snooping the original value

image

Done that way, a user can generate a hash for their password, and pass that to the ops for further management without disclosing the actual secret at any time (not in the repo, not in the manifest, not in the kubernetes api server), thus applying the principle of least privilege.

The implementation is conceptually similar to nginx, which takes its basic auth credentials from htpasswd files where the secrets are hashed with bcrypt.

@jacobsvante
Copy link
Owner

Ah, I'm sorry. I didn't notice that part. I'll make a code review now.

@immerrr immerrr requested a review from jacobsvante December 4, 2021 09:40
@immerrr
Copy link
Author

immerrr commented Dec 4, 2021

@jacobsvante I have addressed your feedback (either in code or as a comment), please, let me know WDYT

@codecov
Copy link

codecov bot commented Dec 27, 2021

Codecov Report

Merging #2 (b38bb79) into main (f670233) will decrease coverage by 1.47%.
The diff coverage is 90.14%.

Impacted file tree graph

@@             Coverage Diff             @@
##              main       #2      +/-   ##
===========================================
- Coverage   100.00%   98.52%   -1.48%     
===========================================
  Files            9       10       +1     
  Lines          411      474      +63     
===========================================
+ Hits           411      467      +56     
- Misses           0        7       +7     
Impacted Files Coverage Δ
fastapi_security/cli.py 83.87% <83.87%> (ø)
fastapi_security/basic.py 95.34% <93.10%> (-4.66%) ⬇️
fastapi_security/api.py 100.00% <100.00%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update f670233...b38bb79. Read the comment docs.

Copy link
Owner

@jacobsvante jacobsvante left a comment

Choose a reason for hiding this comment

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

Perfect, I love your solution. Thanks for the hard work. Can you fix the failing tests and I'll merge?

Comment on lines +34 to +41
def test_gendigest_without_params():
result = subprocess.run(["fastapi-security", "gendigest"], capture_output=True)
assert result.returncode == 2
assert result.stdout.decode().splitlines() == []
assert result.stderr.decode().splitlines() == [
"usage: fastapi-security gendigest [-h] --salt SALT",
"fastapi-security gendigest: error: the following arguments are required: --salt",
]
Copy link
Owner

Choose a reason for hiding this comment

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

fastapi-security command is not found when I run poetry run pytest. Perhaps call cli.main directly like you've done in test_gendigest_smoke_test? E.g:

Suggested change
def test_gendigest_without_params():
result = subprocess.run(["fastapi-security", "gendigest"], capture_output=True)
assert result.returncode == 2
assert result.stdout.decode().splitlines() == []
assert result.stderr.decode().splitlines() == [
"usage: fastapi-security gendigest [-h] --salt SALT",
"fastapi-security gendigest: error: the following arguments are required: --salt",
]
def test_gendigest_without_params(capsys):
with pytest.raises(SystemExit, match="2"):
cli.main(["gendigest"])
captured = capsys.readouterr()
assert captured.out == ""
assert captured.err.splitlines() == [
"usage: fastapi-security gendigest [-h] --salt SALT",
"fastapi-security gendigest: error: the following arguments are required: --salt",
]

Comment on lines +20 to +23
main_parser = argparse.ArgumentParser(
description=_wrap_paragraphs(__doc__),
formatter_class=argparse.RawDescriptionHelpFormatter,
)
Copy link
Owner

Choose a reason for hiding this comment

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

prog needs to be added for unit test change suggestion I'm making below to work.

Suggested change
main_parser = argparse.ArgumentParser(
description=_wrap_paragraphs(__doc__),
formatter_class=argparse.RawDescriptionHelpFormatter,
)
main_parser = argparse.ArgumentParser(
prog="fastapi-security",
description=_wrap_paragraphs(__doc__),
formatter_class=argparse.RawDescriptionHelpFormatter,
)

Comment on lines +7 to +15
def test_usage_output_without_params():
result = subprocess.run(["fastapi-security"], capture_output=True)
assert result.returncode == 2
assert result.stdout.decode().splitlines() == []
assert result.stderr.decode().splitlines() == [
"usage: fastapi-security [-h] {gendigest} ...",
"fastapi-security: error: the following arguments are required: subcommand",
]

Copy link
Owner

Choose a reason for hiding this comment

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

To fix failing tests you could call cli.main directly as I've suggested below. Makes for faster tests as well.

Copy link
Author

Choose a reason for hiding this comment

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

The failure was intentional, so to speak, but honestly I forgot about the pending change I intended to make. It is fixed now.

The problem is not in calling cli.main directly or not, but rather in that add_subparsers function only added required= kwarg in Python 3.7, so I wanted a test suite that would work with required=True in 3.7+ to then mimic the same behaviour (message and exit-code) for 3.6. See my most recent commit.

The reasons I went for subprocess approach in the first place are as follows:

  • it does not require hard-coding the prog= kwarg
  • it doubles as an integration test for the entire script (also testing that the entrypoint configuration is correct)
  • it does not require catching a SystemExit exception to ensure tests continue working

If it were up to me, I'd probably keep the existing tests as is, and use cli.main as soon as there are more unit tests to add. Although if you insist I can change go for cli.main in this PR as well.

Copy link
Owner

@jacobsvante jacobsvante Jan 5, 2022

Choose a reason for hiding this comment

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

@immerrr Gotcha. Let's drop support for 3.6, it's reached end-of-life. You can include that change in your PR. Or if you'd rather I remove it and you rebase on top of my commit?

Copy link
Author

Choose a reason for hiding this comment

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

Done (and reverted the change that enabled support for py3.6)

Copy link
Owner

Choose a reason for hiding this comment

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

Thanks @immerrr. Tests are still failing on Windows. And they also fail locally on my Mac OS. Fails to find command fastapi-security. I would suggest that you go the cli.main route.

Copy link
Author

Choose a reason for hiding this comment

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

ok, will do

Comment on lines +17 to +31
def test_usage_with_help_param():
result = subprocess.run(["fastapi-security", "-h"], capture_output=True)
assert result.returncode == 0
assert result.stdout.decode().splitlines() == [
"usage: fastapi-security [-h] {gendigest} ...",
"",
"fastapi_security command-line interface",
"",
"positional arguments:",
" {gendigest} Specify a sub-command",
"",
"optional arguments:",
" -h, --help show this help message and exit",
]
assert result.stderr.decode().splitlines() == []
Copy link
Owner

Choose a reason for hiding this comment

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

Same with this one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants