LoginSignup
1
1

More than 1 year has passed since last update.

Python: urllib3 で HTTP 認証 (Basic/Digest) する

Last updated at Posted at 2022-08-06

urllib や requests では認証用のクラスを指定してフックするだけで HTTP 認証をおこなうことができるが、urllib3 には HTTP 認証機能がないため、自前で実装してみた。

認証シーケンス

認証なしでリクエストすると WWW-Authenticate ヘッダを含む 401 応答が返ってくるので、それを元に Authorization ヘッダをつけて再度要求する。Digest 認証の場合は WWW-Authenticate ヘッダに含まれる情報を元に計算したものを返す必要があるが、Basic 認証の場合はいきなり Authorization ヘッダをつけて投げても良い。

実装例

Basic 認証

直接 Authorization ヘッダをつけてリクエストする場合

class HTTPBasicAuth:
    @staticmethod
    def build_authorization(username: str, password: str):
        return "Basic " + base64.b64encode(f"{username}:{password}".encode("utf-8")).decode("utf-8")


def get(endpoint: str, username: str, password: str):
    http = urllib3.PoolManager()
    res = http.request("GET", endpoint, headers={
        "Authorization": HTTPBasicAuth.build_authorization(username, password),
    })

Basic/Digest 認証

WWW-Authenticate が返ってきたら Authorization ヘッダをつけてリトライ

class HTTPDigestAuth:
    def __init__(self):
        self.cnonce = "".join([random.choice('0123456789abcdef') for x in range(32)])
        self.nc: int = 0

    @staticmethod
    def parse_www_authenticate(www_authenticate: str) -> Optional[dict]:
        if www_authenticate[:6].lower() != "digest":
            return None
        result = {}
        for param in re.split(r",\s*", www_authenticate[7:]):
            (k, v) = param.split("=", 2)
            if v[0] == "\"" and v[-1] == "\"":
                v = v[1:-1]
            result[k] = v
        return result

    @staticmethod
    def calculate_response(realm: str, nonce: str, qop: str, cnonce: str, nc: int, method: str, uri: str, username: str, password: str) -> str:
        def md5hex(a: str):
            return md5(a.encode("utf-8")).hexdigest()

        a1 = f"{username}:{realm}:{password}"
        a2 = f"{method}:{uri}"
        a3 = f"{md5hex(a1)}:{nonce}:{nc:08d}:{cnonce}:{qop}:{md5hex(a2)}"
        return md5hex(a3)

    def build_authorization(self, method: str, uri: str, www_authenticate: str, username: str, password: str) -> Optional[str]:
        www_authenticate = self.parse_www_authenticate(www_authenticate)
        if not www_authenticate:
            return None
        if www_authenticate["algorithm"].lower() != "md5" or www_authenticate["qop"] != "auth":
            return None  # unsupported

        self.nc += 1
        response = self.calculate_response(
            www_authenticate["realm"], www_authenticate["nonce"], www_authenticate["qop"],
            self.cnonce, self.nc, method, uri, username, password
        )
        return \
            "Digest username=\"{}\", realm=\"{}\", nonce=\"{}\", uri=\"{}\"" \
            ", algorithm={}, qop={}, cnonce=\"{}\", nc={:08d}, response=\"{}\"".format(
                username,
                www_authenticate["realm"],
                www_authenticate["nonce"],
                uri,
                www_authenticate["algorithm"],
                www_authenticate["qop"],
                self.cnonce,
                self.nc,
                response
            )



def get(base_url: str, path: str, username: str, password: str):
    http = urllib3.PoolManager()
    endpoint = f"{base_url}{path}"
    res = http.request("GET", endpoint)
    if res.status == 401 and "WWW-Authenticate" in res.headers:
        www_authenticate_header = res.headers["WWW-Authenticate"]
        authorization = None
        www_authenticate_basic = www_authenticate_header.find("Basic ")
        www_authenticate_digest = www_authenticate_header.find("Digest ")
        if www_authenticate_basic >= 0 and AUTH_TYPE == "basic":
            # Basic 認証
            authorization = HTTPBasicAuth.build_authorization(
                username, password
            )
        elif www_authenticate_digest >= 0 and AUTH_TYPE == "digest":
            # Digest 認証
            authorization = digest_auth.build_authorization(
                "GET", PATH, www_authenticate_header[www_authenticate_digest:], username, password
            )
        if authorization:
            res = http.request("GET", endpoint, headers={
                "Authorization": authorization
            })
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1