GoでBasic認証を実装する

f:id:daihase:20190809022658p:plain

こんにちは、daihaseです。

久々のブログ記事です。今日はGolangを使ってBasic認証を実装してみます。Basic認証はご存知、ユーザー名パスワードを組み合わせた非常にシンプルな認証方式です。管理画面なんかでもよく見ますね。

こちら、Golangではnet/httpパッケージで簡単に実現出来ます。早速見てみましょう。

import (
    "crypto/subtle"
    "fmt"
    "log"
    "net/http"
)

const (
    CONN_HOST      = "localhost"
    CONN_PORT      = "8080"
    ADMIN_USER     = "admin"
    ADMIN_PASSWORD = "password"
)

func helloWorld(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello World")
}

func BasicAuth(handler http.HandlerFunc, realm string) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        user, pass, ok := r.BasicAuth()
        if !ok || subtle.ConstantTimeCompare([]byte(user),
            []byte(ADMIN_USER)) != 1 || subtle.ConstantTimeCompare([]byte(pass),
            []byte(ADMIN_PASSWORD)) != 1 {
            w.Header().Set("WWW-Authenticate", `Basic realm="`+realm+`"`)
            w.WriteHeader(401)
            w.Write([]byte("認証に失敗しました。\n"))
            return
        }
        handler(w, r)
    }
}

func main() {
    http.HandleFunc("/", BasicAuth(helloWorld, "ユーザー名とパスワードを入力してください。"))
    err := http.ListenAndServe(CONN_HOST+":"+CONN_PORT, nil)
    if err != nil {
        log.Fatal("error starting http server : ", err)
        return
    }
}

 

まずGOPATHの通った場所に、basicauth.goという名前でファイルを作成します。 特に特別な実装をしてる部分があるわけでもないですが、1点注意点があります。

Basic認証で比較するユーザ名パスワード、こちら実際に認証で通る正しいものをconstのADMIN_USERADMIN_PASSWORDで定義しているわけですが、実際上のユーザー名、パスワードのフォームにそれぞれ値を打ち込んでLog InするとBasicAuth関数内のuserpassに値がセットされます。

ここで、

func BasicAuth(handler http.HandlerFunc, realm string) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        user, pass, ok := r.BasicAuth()
        if !ok || user != ADMIN_USER || pass != ADMIN_PASSWORD {
...
..
.

 

のように、 !=== で単純に比較したくなると思いますが、それだとセキュリティ上の問題が出てきます。

タイミングアタック(Timing Attack)と呼ばれるもので、簡単に説明するとある文字列と文字列を比較する際に先頭から1文字ずつ比較し、異なった場合falseとする単純比較だと比較対象となる文字列の長さによって演算回数が変わるため、何度も値を変えては計測し、というのを厳密に繰り返していけば比較元の文字列を推測出来てしまうんですね。結果、一致してしまい認証が通ってしまいます。

そこでGoでは値の長さに関わらず一定の時間で処理を行うことで、そうした推測を出来ないようにする便利関数が用意されてるので、そちらを使っています。

それがsubtle.ConstantTimeCompareですね。

func ConstantTimeCompare(x, y []byte) int

 

これは引数のx、yそれぞれの引数スライスを比較し、同じ長さで等しい場合のみ1を返し、そうでない場合は0を返す関数です。この関数を使って安全に比較を行い、違う場合は認証に失敗した旨を画面に表示するように実装しています。

正しいユーザー名、パスワードを入力した場合はhelloWorld関数内で実装したFprintfの内容が出力されます。

     

どうでしょう? 思ったより簡単にBasic認証が実装出来たかと思います。Goはパッケージで便利なものが沢山揃ってるので開発が捗っていいですね。

それでは良い開発ライフを〜