golang echoでのユーザー認証にDigest認証を利用する

## はじめに

golangのechoを使ってアカウント付きのサービスを作成中。
利用するアカウント情報がそこまで重要なものでは無いので、多段認証みたいな凝ったものはいらないかなと思いつつ、
かといって生パスワードをDBに突っ込んでおくのもセキュリティ観点で嫌だなと思ったので、Digest認証を仕込んでみました。

## echoのURLハンドリング

まずはechoの使い方を簡単に説明。
[echo](https://github.com/labstack/echo)は以下でインストールして使用します。

```:インストール
go get -u github.com/labstack/echo
```

使う際は、こんな感じでURLのGroupを作ってからその下のURLに対して対応関数を登録 or 直URL指定で関数登録をします。

```go:main.go
package main
import (
    "net/http"
    "github.com/labstack/echo"
)

func HandleIndex(c echo.Context) error{
    return c.JSON(http.StatusOK, map[string]interface{}{"hello": "world"})
}

func main() {
    // Echoのインスタンスを生成
    e := echo.New()

    /*...*/

    // 各ルーティングに対するハンドラを設定
    group := e.Group("/api/")
    // http://IPアドレス/api/xxxに対するアクセス時のハンドラー登録。HandleIndexが呼ばれる
    group.Any("index", HandleIndex)

    // GET /アクセス時のハンドラー登録。HandleIndexが呼ばれる
    e.GET("/", HandleIndex)

    // サーバーを開始
    e.Start(":3000")
}
```

## Digest認証

[abbot/go-http-auth](https://github.com/abbot/go-http-auth)を利用しました。

```:インストール
go get github.com/abbot/go-http-auth
```

同リポジトリコード内にサンプルがあります。`func handle(w http.ResponseWriter, r *auth.AuthenticatedRequest)`という定義の関数を`Wrap`を介して登録します。
Digest認証の為のユーザーと紐づくパスワードを取得する関数はNewDigestAuthenticatorで指定します。
また、sampleの"b98e16cbc3d01734b264adba7baa3bf9"は、Digest認証で使用する"ユーザー名:realm値:パスワード"のMD5ハッシュ化した値となります。
デフォルトはハッシュ計算済みの文字列を返す関数を作る形。

```go:main.go
package main

import (
    "fmt"
    "net/http"

    auth ".."
)

func secret(user, realm string) string {
    if user == "john" {
        // password is "hello"
        return "b98e16cbc3d01734b264adba7baa3bf9"
    }
    return ""
}

func handle(w http.ResponseWriter, r *auth.AuthenticatedRequest) {
    fmt.Fprintf(w, "<html><body><h1>Hello, %s!</h1></body></html>", r.Username)
}

func main() {
    authenticator := auth.NewDigestAuthenticator("example.com", secret)
    http.HandleFunc("/", authenticator.Wrap(handle))
    http.ListenAndServe(":8080", nil)
}
```

生パスワードを使いたい場合は`authenticator.PlainTextSecrets=true`を設定すればOKです。

## echoでDigest認証を利用する

本題です。echoでDigest認証を利用したいのですが、echoとgo-http-authで利用するhandleの型が違います。
なので、go-http-authの中身を参考にしてwrapper関数を用意しました。

```go:auth.go
package controller

import (
    "net/http"
    "github.com/labstack/echo"
    "github.com/abbot/go-http-auth"
)

func authenticate(user, realm string) string {
//パスワード取得 && return処理
}

//Digest認証で使用する構造体
var authenticator * auth.DigestAuth = authwrapNewDigestAuthenticator()

//auth.NewDigestAuthenticator直呼びでもいいけど、auth.DigestAuthの初期値を変えたい場合を考慮してラッパー化
func authwrapNewDigestAuthenticator() *auth.DigestAuth {
    res := auth.NewDigestAuthenticator("example.com", authenticate)
    return res
}

//auth.Wrapの中身を改良してechoフォーマットを扱えるようにしました。
func DigestAuthenticate(input func(c echo.Context, r *auth.AuthenticatedRequest) error) echo.HandlerFunc{
    return func(c echo.Context) error {
        r := c.Request()
        w := c.Response().Writer
        if username, authinfo := authenticator.CheckAuth(r); username == "" {
            authenticator.RequireAuth(w, r)
            return echo.NewHTTPError(http.StatusUnauthorized, "Please write collect username and password")
        } else {
            ar := &auth.AuthenticatedRequest{Request: *r, Username: username}
            if authinfo != nil {
                w.Header().Set(authenticator.Headers.V().AuthInfo, *authinfo)
            }
            return input(c, ar)
        }
    }
}

//認証なしの場合の変換処理
func NoAuthenticate(input func(c echo.Context, r *auth.AuthenticatedRequest) error) echo.HandlerFunc{
    return func(c echo.Context) error {
        return input(c, nil);
    }
}
```

handler関数は引数にauth.AuthenticatedRequestを追加しています。
また、利用する際はauth.Wrapのようにhandler関数をDigestAuthenticate or NoAuthenticateで変換して使います。

```go:main.go
//認証情報を使うならrを参照
func HandleIndex(c echo.Context, r *auth.AuthenticatedRequest) error{
    return c.JSON(http.StatusOK, map[string]interface{}{"hello": "world"})
}

func main() {
    // Echoのインスタンスを生成
    e := echo.New()

    // 各ルーティングに対するハンドラを設定
    group := e.Group("/api/")
    group.Any("index", controller.DigestAuthenticate(HandleIndex))
    e.GET("/", controller.NoAuthenticate(HandleIndex))

    // サーバーを開始
    e.Start(":3000")
}
```

## 余談

本当はMD5は弱いハッシュアルゴリズムなので何か簡易的なものを自作しようかとも考えていたんですが、RFC 7616でSHA-256, SHA-512-256等が使え、パッケージにも[pull request](https://github.com/abbot/go-http-auth/pull/49/commits/3776c26a72aa8ab61b77f53528dc807990ff54bf)が出ていたのでこれを活用すればいいかなと。しかし実際試してみたところ動かず。

調べてみるとGoogle chromeもFireFoxもDigest認証の拡張サポートをする予定はないとのこと。
SSL/TLSだったり多段認証を使えよって感じなんだろうな。

この辺調べてて面白かったので、そのままDigest認証を使うことにしました。

## 参考

[Go言語でWebサイトを作ってみる：目次](https://qiita.com/y_ussie/items/8fcc3077274449478bc9) (各項目参考にさせていただきました
[SHA 256 Digest Authentication](https://bugzilla.mozilla.org/show_bug.cgi?id=472823)