psf / psf/requests

requests.auth.HTTPDigestAuth does not support qop=auth-int

Open
#5,692 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
54.3k
Forks
10.4k
Avg merge
16h 43m
Merged PRs (30d)
3

Description

Summary:

When using requests.auth.HTTPDigestAuth, if the www-authenticate digest back has a qop value of 'auth-int', build_digest_header returns None, which makes http.client.HTTPConnection.putheader throw an exception because *values expects to be a set of Strings or Bytes.

Expected Result:

I expect a valid Digest in return, or at least an Exception that input data is bad

Actual Result:

TypeError Exception - expected string or bytes-like object

Reproduction Steps

import requests
requests.get('https://device.hostname/api/auth', auth=requests.auth.HTTPDigestAuth('login','password')

System Information

$ python -m requests.help
{
  "chardet": {
    "version": "3.0.4"
  },
  "cryptography": {
    "version": ""
  },
  "idna": {
    "version": "2.10"
  },
  "implementation": {
    "name": "CPython",
    "version": "3.8.6"
  },
  "platform": {
    "release": "20.2.0",
    "system": "Darwin"
  },
  "pyOpenSSL": {
    "openssl_version": "",
    "version": null
  },
  "requests": {
    "version": "2.24.0"
  },
  "system_ssl": {
    "version": "1010108f"
  },
  "urllib3": {
    "version": "1.25.9"
  },
  "using_pyopenssl": false
}

Working Fix

requests.auth.HTTPDigestAuth.build_digest_header has the following code, which is causing the return of None:

        if not qop:
            respdig = KD(HA1, "%s:%s" % (nonce, HA2))
        elif qop == 'auth' or 'auth' in qop.split(','):
            noncebit = "%s:%s:%s:%s:%s" % (
                nonce, ncvalue, cnonce, 'auth', HA2
            )
            respdig = KD(HA1, noncebit)
        else:
            # XXX handle auth-int.
            return None

Patches Needed:
requests.auth.HTTPDigestAuth.build_digest_header needs to have an optional arg for body, which should be the bytes of the body of the request or None

def build_digest_header(self, method, url, body=b''):

The qop value should be put into a list:

        qop = self._thread_local.chal.get('qop')
        if qop:
            qop = [x.strip() for x in qop.split(',')]

A2 needs to be calculated differently if auth-int:

        if qop and 'auth-int' in qop:
            A2 = '%s:%s:%s' % (method, path, hash_utf8(body or b''))
        else:
            A2 = '%s:%s' % (method, path)

And the qop test block needs to be changed:

        if not qop:
            respdig = KD(HA1, "%s:%s" % (nonce, HA2))
        else:
            if 'auth' in qop or 'auth-int' in qop:
                noncebit = "%s:%s:%s:%s:%s" % (
                    nonce, ncvalue, cnonce, ','.join(qop), HA2
                )
                respdig = KD(HA1, noncebit)
            else:
                raise NotImplementedError('qop value of "%s" is not implemented.' % (','.join(qop)))

Also the last check before returning digest:

        if qop:
            base += ', qop=%s, nc=%s, cnonce="%s"' % (','.join(qop), ncvalue, cnonce)

Now to change the callers:
handle_401 needs to be changed to add the body arg:

            prep.headers['Authorization'] = self.build_digest_header(
                prep.method, prep.url, prep.body)

call needs to be changed to add the body arg:

            r.headers['Authorization'] = self.build_digest_header(r.method, r.url, r.body)

And that should create a fully working framework for auth-int with RFC 2617

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in requests.auth.HTTPDigestAuth.build_digest_header and trace its callers in handle_401 and call, focusing on how the request body reaches digest calculation. Support qop=auth-int using the request body as described and ensure unsupported qop values raise an exception instead of returning None; done when the reproduction no longer produces the TypeError and digest authentication remains valid for existing qop cases.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
authentication, networking
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.