Digest認証の確認環境、サーバ側とiOSクライアント環境


発端

teratailの「Alamofireでdigest認証について」という質問でした。今までhttps+Basic認証しかやったことがなく、Digest認証自体知りませんでした。また、開発効率を考えswiftでWebアクセスすると言えばAlamofireのみでしかやっていなかったので、もう少しコア技術についても知りたいという点もあり、実際に調査してみたのが発端で、その過程で確認環境などを構築したので、それを公開してしまおうという流れです。

一式

githubに以下の説明に利用したファイル一式をアップしています。説明で省いた詳細をご確認いただきたい方はご覧ください。

サーバ側

nginxはデフォルトではDigest認証に対応していない様で、手っ取り早くNode.jsで実装しようとしたけれど、Node.jsを普段使いしていないので、全く手っ取り早くできず断念。。最終的にApache2.4で構築することに。

細かな設定は、既に色々と情報があるのでそちらに任せますが(例えば)、ポイントだけ提示したいと思います。

httpd.conf

dockerイメージのhttpd:2.4を使いますが、httpd.confはそのイメージに含まれているものをコピーして必要な部分を追記修正しました。
conf/extraに追記分だけ入れたいところですが、デフォルトでIncludeされているのは、conf/extra/proxy-html.confだけだったので、影響範囲を最小限にするという意図で直接httpd.confを修正してしまっています。

httpd.conf
# digest認証用のモジュールをロード
LoadModule auth_digest_module modules/mod_auth_digest.so

# 認証をかけたい場所(例として/digestとしている)に以下を設定
# さらに例としてhttpd.confに記載しているが、もちろん.htaccessでも良い
<Location /digest>
    AuthType Digest
    AuthName "private Web"
    AuthUserFile "/usr/local/apache2/conf/.htdigest"
    Require valid-user
</Location>

LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Authorization}i\"" combined

Logフォーマット

発端となったDigest認証時のalgorithmがどの様に設定されているのかを表示させるため、LogFormatでAuthorizationヘッダを表示する様にしています。

docker

基本は上記のhttpd.confを読み込ませる様にする、そして認証するためのディレクトリを公開します。
./auth -> /usr/local/apache2/conf
./webroot -> /usr/local/apache2/htdocs

.htdigest

サーバ側で認証する際のユーザとパスワードを管理するファイルになります、名前はなんでもよく、前述したconfのAuthUserFileで指定したものになります。
Basic認証ではhtpasswdを利用することと同様にDigest認証でもhtdigestコマンドがあり、それを利用すれば良いのだけれど、自動化する際にはhtpasswdより使い勝手が悪いです。

htdigest -c .htdigest [[AuthName]] USERNAME

パスワードを標準入力から設定する必要があることと、結果に不要なデータ(’ー’)が含まれていたりします。
また、今回の環境作成では可能な限りシンプルに構築したかったので、別の方法を採用しています。結局MD5でハッシュしたものとのことと、環境依存性も考慮し、docker内で行いました。

docker-compose

最終的に、上記の様な環境を構築するためにdocker-composeを利用しました。そのymlを以下に提示します。

docker-compose.yml
version: "2"
services:
  digest:
    image: "httpd:2.4"
    environment:
      - HTTPD_DIGEST_PASS=pass
      - HTTPD_DIGEST_USER=admin
      - HTTPD_DIGEST_NAME=private Web
    volumes:
      - conf:/usr/local/apache2/conf
      - web:/usr/local/apache2/htdocs
    command: >
      bash -c 'echo -n $$HTTPD_DIGEST_USER:$$HTTPD_DIGEST_NAME:>conf/.htdigest &&
               echo -n $$HTTPD_DIGEST_USER:$$HTTPD_DIGEST_NAME:$$HTTPD_DIGEST_PASS|md5sum -|cut -f 1 -d " ">>conf/.htdigest
              '
  apache:
    image: "httpd:2.4"
    hostname: mobilework.kddilabs.jp
    ports:
      - 8888:8888
    volumes:
      - conf:/usr/local/apache2/conf
      - web:/usr/local/apache2/htdocs
    depends_on:
      - digest

volumes:
  conf:
    driver_opts:
      type: none
      device: $PWD/auth
      o: bind
  web:
    driver_opts:
      type: none
      device: $PWD/webroot
      o: bind

最初のdigestで.htdigestを生成、apacheでhttpサーバ用インスタンスを起動します。

一応正常にサーバ側が構築できたのかを確認するためにcurlを利用します。

curl -v --digest -u admin:pass http://localhost:8888/digest/

上記のコマンドでwebroot/digest/index.htmlに置いたindex.htmlの内容が表示されたらOKとなります。うまく動作しない場合には、標準出力に表示されたログを参照するとか、webroot/error内容を確認すると良いでしょう。

クライアント側

swiftでの実装ですが、基本はAppleのマニュアルFetching Website Data into Memoryを参考にしています。
Completion HandlerとDelegateを利用する2種類の方法があるが、今回詳細な状況を把握するためにDelegateを利用しました。URLSessionDataDelegateURLSessionTaskDelegateの二つで良いようです。

今回のポイントとして、認証時に呼び出されるurlSession(_:task:didReceive:completionHandler:)が重要となります。

    func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        print(#file, #function, #line, separator:":")
        print("authentication method: ", challenge.protectionSpace.authenticationMethod)
        print("protection space , host: ", challenge.protectionSpace.host)
        let cnt = challenge.previousFailureCount
        print("previous failure count: ", cnt)
        guard cnt == 0 else {
            completionHandler(.cancelAuthenticationChallenge, nil)
            return
        }
        let cred = URLCredential(user: "admin", password: "pass", persistence: .forSession)
        completionHandler(.useCredential, cred)
    }

URLCredentialのインスタンス生成時に引き渡すユーザ・パスワードを.htdigest作成時と一致させることになります。

クライアント側は短いですがこれだけ。ソースを見るのが一番ですね。