-
-
Notifications
You must be signed in to change notification settings - Fork 2k
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
Added a digest authentication helper #2213
base: master
Are you sure you want to change the base?
Conversation
Sorry, I don't follow how to use the helper. |
Sorry, I meant to add this snippet as well. import aiohttp
import asyncio
client = aiohttp.ClientSession()
auth = aiohttp.auth.DigestAuth('usr', 'psswd', client)
async def fetch(url, **kwargs):
return await auth.request('GET', url, **kwargs)
loop = asyncio.get_event_loop()
response = loop.run_until_complete(fetch('http://httpbin.org/digest-auth/auth/usr/psswd/MD5/never')) |
Obviously, I can add tests, documentation, etc. Before I go through all that though, I just wanted to see if there is any interest in it. |
Well, idea is good but it should be proved by test suite. |
This is a good idea!
|
@Alxpy |
@kxepal yes, that makes sense, I agree |
That was the kind of discussion I was looking for. It sounds like you guys would prefer to keep it separated like it currently is. I'll go ahead and add the tests and documentation and then update the pull request. Thanks. |
aiohttp/helpers.py
Outdated
self.args = {} | ||
self.session = session | ||
|
||
@asyncio.coroutine |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nobody uses this syntax here anymore.
…ion of method in docs
So I updated the pull request to following the contributing guidelines (test, docs, etc.). Since it has been a while I also went ahead and merged the latest from aio-libs/aiohttp:master into the pull request. It looks like you guys switched over to the async/await syntax so I did that as well. However, half of all the tests are failing now. It looks like it's that way for all the tests though, so I'm assuming it isn't an issue. Let me know if there is anything else you want me to do. |
aiohttp/helpers.py
Outdated
def KD(s, d): | ||
return H('%s:%s' % (s, d)) | ||
|
||
parsed = urlparse(url) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have yarl. Why url parse?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was unfamiliar with yarl; I will switch it over.
if nonce == self.last_nonce: | ||
self.nonce_count += 1 | ||
else: | ||
self.nonce_count = 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This branch is redundant. 0 +1 equals 1.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you re-use the instance, the nonce_count
count can grow without bound. The nonce count is important in preventing replay attacks. There probably aren't too many cases where the nonce will need to reset but perhaps in the event of some failed messages.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I should clarify, if you re-use the instance of DigestAuth
for performing multiple requests.
aiohttp/helpers.py
Outdated
k = str(self.nonce_count).encode() | ||
k += nonce.encode() | ||
k += time.ctime().encode() | ||
k += os.urandom(8) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about join?
aiohttp/helpers.py
Outdated
) | ||
respdig = KD(HA1, noncebit) | ||
else: | ||
raise client_exceptions.ClientError( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
qop
is known quite early above. How about to fail fast?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There would have to be some duplication of logic around qop
and I didn't think that the performance was that big of an issue. I can move it though.
aiohttp/helpers.py
Outdated
return response | ||
|
||
|
||
if PY_352: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No longer actual. We require 3.5.3+ on master.
tests/test_helpers.py
Outdated
def handler(request): | ||
realm = '[email protected]' | ||
if 'Authorization' in request.headers: | ||
pattern = re.compile(r'digest ', flags=re.IGNORECASE) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
triple times.
tests/test_helpers.py
Outdated
def handler(request): | ||
realm = '[email protected]' | ||
if 'Authorization' in request.headers: | ||
pattern = re.compile(r'digest ', flags=re.IGNORECASE) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, multiple ones.
tests/test_helpers.py
Outdated
@asyncio.coroutine | ||
def test_digest_auth_MD5_sess(loop, test_client): | ||
opaque = binascii.hexlify(os.urandom(16)) | ||
username = 'usr' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wh usr nd psswd whl username nd password wrwhr ls?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was trying to demonstrate that the helper isn't just hardcoded to work with one username and password. Although, this really doesn't challenge it at all. Once you decide what you want to do about the string escaping I can change this to include something more difficult like spaces and quotes.
tests/test_helpers.py
Outdated
def handler(request): | ||
realm = '[email protected]' | ||
if 'Authorization' in request.headers: | ||
pattern = re.compile(r'digest ', flags=re.IGNORECASE) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
again
tests/test_helpers.py
Outdated
def handler(request): | ||
realm = '[email protected]' | ||
if 'Authorization' in request.headers: | ||
pattern = re.compile(r'digest ', flags=re.IGNORECASE) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, tests are the same as the regular code - please, show it some love in the name of maintenance.
Tests still are red :( |
headers = {} | ||
|
||
# Save the args so we can re-run the request | ||
self.args = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you store last request as a class attribute? Won't it cause everything to break when DigestAuth
instance is used by multiple coroutines simultaneously?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, that's a good point. We can just store it as a local variable and then pass it to _handle_401
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this applies to self.challenge
as well.
|
||
# Only try performing digest authentication if the response status is | ||
# from 400 to 500. | ||
if 400 <= response.status < 500: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In one of my tests, when username-password pair was incorrect, this led to infinite recursion. I think the fix is to call _handle_401
only if self.challenge
is None
.
nonce, | ||
time.ctime(), | ||
os.urandom(8).decode(errors='ignore'), | ||
]).encode() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
os.urandom(8).decode(errors='ignore')
looks really odd. It basically discards characters that cannot be decoded as UTF-8, so the string is usually shorter than 8 characters. This would be better, I think:
cnonce_data = ''.join([
str(self.nonce_count),
nonce,
time.ctime(),
]).encode()
cnonce_data += os.urandom(8)
Hi guys, What do you think the status of this pull request is and do you have a rough idea if it is going to be ready any time soon-ish? It seems quite close from an external eye... At the moment AFAIK there is no pure asyncio framework supporting digest authentication, you have to either go back to Thanks! |
I like the idea but have a feeling that we need to use slightly different and not existing yet API for it. |
@RouquinBlanc I'm also really looking forward to have DigestAuth available in aiohttp since it is my favorite async http lib. |
@asvetlov The Digest header can include spaces in realm name, for example: My suggestion as per below:
But this is probably also not bulletproof as the realm could include also comma. And if the above make sense, then the "remove trailing comma" section in parse_pair(pair) should also be updated accordingly:
|
@sonicblind thanks for the comment. |
Still nothing new. I think it would be amazing if you complete this feature request. |
|
||
.. class:: DigestAuth(login, password', session) | ||
|
||
HTTP digest authentication helper. Unlike :class:`DigestAuth`, this helper |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unlike BasicAuth
?
def parse_key_value_list(header): | ||
return { | ||
key: value for key, value in | ||
[parse_pair(header_pair) for header_pair in header.split(' ')] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Splitting on space does not work because values can be quoted strings that contain spaces.
E.g. 'realm="Login to AMC072A731527963121B", qop="auth", nonce="839506863", opaque="f5a15b51387c97add984d4d948213e563fe3eede"'
I still don't see |
This would be of tremendous value... any chance of merging? |
Hey @feus4177, are you planning to resume this effort? It looks like a lot of reviews accumulated over the years. Plus, there's merge conflicts that would have to be addressed. |
It would be great to get this one implemented as its one of the key drivers for Home Assistant devs choosing alternate (usually slower) libraries because the device they are integrating needs digest auth. |
I have no intention of continuing this work. I tried to get feedback on the overall approach of the work before I invested a bunch of time and effort into finalizing the details: #2213 (comment). After some small feedback I went through and cleaned everything up, made sure all the tests were passing and everything. Then I was hit with this: #2213 (comment). I didn't enjoy jumping through all the hoops just to then revisit the initial feedback I was trying to solicit. I have no intention of going through all that work again knowing that it will all be in vain if the owners decide that they still don't like the architecture of the solution. |
The current authentication interface design needs to accommodate additional future methods, and a redesign is required. Unfortunately, this conclusion was realized late in the review process, and I'm sure you wanted to do something other than sign up to do that redesign. I can understand how that is frustrating. Thank you for your contributions thus far and for following up on this PR. |
@bdraco, I appreciate your understanding. I also think a redesign is completely reasonable. I just think it's up to the maintainers at this point. |
I think there's a discussion elsewhere, but I noted that |
I started thinking about this in #9732 |
What do these changes do?
Added a digest authentication helper.
Are there changes in behavior for the user?
None
Related issue number
Resolves #4939
Checklist
CONTRIBUTORS.txt
changes
folder<issue_id>.<type>
for example (588.bug)issue_id
change it to the pr id after creating the pr.feature
: Signifying a new feature..bugfix
: Signifying a bug fix..doc
: Signifying a documentation improvement..removal
: Signifying a deprecation or removal of public API..misc
: A ticket has been closed, but it is not of interest to users.I didn't go through the checklist. I just wanted to see if this is something you are interested in incorporating into the aiohttp package. To be able to use this DigestAuth helper like the aiohttp BasicAuth helper would take some restructuring so I decided to keep it as isolated as possible right now. Let me know what you think and I can make any changes as necessary.