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

# 発端

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

# 一式
[github](https://github.com/0bara/test_digest_auth)に以下の説明に利用したファイル一式をアップしています。説明で省いた詳細をご確認いただきたい方はご覧ください。

# サーバ側
nginxはデフォルトではDigest認証に[対応していない](http://mogile.web.fc2.com/nginx_wiki/nginx_wiki201510/modules/auth_digest.html)様で、手っ取り早くNode.jsで実装しようとしたけれど、Node.jsを普段使いしていないので、全く手っ取り早くできず断念。。最終的にApache２.4で構築することに。

細かな設定は、既に色々と情報があるのでそちらに任せますが（[例えば](https://qiita.com/mmotoi/items/5d7dcbb9e461feba98cd)）、ポイントだけ提示したいと思います。

## 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](https://developer.apple.com/documentation/foundation/url_loading_system/fetching_website_data_into_memory)を参考にしています。
Completion HandlerとDelegateを利用する2種類の方法があるが、今回詳細な状況を把握するためにDelegateを利用しました。[URLSessionDataDelegate](https://developer.apple.com/documentation/foundation/urlsessiondatadelegate)と[URLSessionTaskDelegate](https://developer.apple.com/documentation/foundation/urlsessiontaskdelegate)の二つで良いようです。

今回のポイントとして、認証時に呼び出される[urlSession(_:task:didReceive:completionHandler:)](https://developer.apple.com/documentation/foundation/urlsessiontaskdelegate/1411595-urlsession)が重要となります。

```swift
    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作成時と一致させることになります。

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