goauth2の認証手順をもっと簡単に

前回の手順と調査

前回のエントリーでも最後に少し書きましたが、goauth2の認証で前回は、

  1. ClientIDの入力
  2. ClientSecretの入力
  3. RedirectURIの入力
  4. 指定されたURLにブラウザでアクセス
  5. 認証コードをコピー
  6. 認証コードを引数に再実行

という手順を踏んでいました。


ClientID、ClientSecretあるいはRedirectURIの入力は仕方がないにしても、ブラウザを開いて、URLを貼り付けて、コードをコピーしてと言うのはどうにかならないものかと思っていたところ、GAE/Gにデプロイするコマンドappcfg.pyが自動的にブラウザを開いて承認ボタンを押すと処理を継続できるのを思い出しました。


それをgoauth2でもできないかというのがきっかけです。


ブラウザを起動するのは、前回勉強したexec.Cmdを使えばできそうなので、承認ボタン押下後のコードをどのように取得するかが問題でした。
いろいろ考えていたところ、このRedirect URIsにlocalhostが記載されていることを思い出しました。



そこでRedirectURIの値をurn:からhttp://localhost/に変更してみたところ、



GETでcodeという名前のパラメータで渡しているではないかと。
であればlocalhostをListenすればコードを取得できるはずなので、それを使用すればそのまま処理を継続できるのではないかと。


というわけで、前回のGolanag Cafeの後半はこの仕組みを実装してみることになりました。

実装結果

上記仕組みで何とか実装することができました。
主要部分のコードだけ記載しておきます。すべてのコードはこちらに置いてありますので参照してください。
goauth2google-api-go-clientを使用しているので、

$ go get code.google.com/p/goauth2/oauth
$ go get code.google.com/p/google-api-go-client/calendar/v3

しておいてください。
google-api-go-clientの方はcalendarを指定しているのになぜかすべて落ちてきます。

func getAuthCode(config *oauth.Config, lsConfig LocalServerConfig) (string, error) {
    url := config.AuthCodeURL("")

    var cmd *exec.Cmd

    switch runtime.GOOS {
    case "windows":
        url = strings.Replace(url, "&", `^&`, -1)
        cmd = exec.Command("cmd", "/c", "start", url)

    case "darwin":
        url = strings.Replace(url, "&", `\&`, -1)
        cmd = exec.Command("open", url)

    default:
        return "", fmt.Errorf("ブラウザで以下のURLにアクセスし、認証して下さい。\n%s\n", url)
    }

    redirectResult := make(chan RedirectResult, 1)
    serverStarted := make(chan bool, 1)
    //
    go func(rr chan<- RedirectResult, ss chan<- bool, p int) {
        http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
            code := r.URL.Query().Get("code")

            if code == "" {
                rr <- RedirectResult{Err: fmt.Errorf("codeを取得できませんでした。")}
            }

            fmt.Fprintf(w, `<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
</head>
<body onload="window.open('about:blank','_self').close();">
ブラウザが自動で閉じない場合は手動で閉じてください。
</body>
</html>
`)
            rr <- RedirectResult{Code: code}
        })

        host := fmt.Sprintf("localhost:%d", p)

        fmt.Printf("Start Listen: %s\n", host)
        ss <- true

        err := http.ListenAndServe(host, nil)

        if err != nil {
            rr <- RedirectResult{Err: err}
        }
    }(redirectResult, serverStarted, lsConfig.Port)

    <-serverStarted

    // set redirect timeout
    tch := time.After(time.Duration(lsConfig.Timeout) * time.Second)

    fmt.Println("Start your browser after 2sec.")

    time.Sleep(2 * time.Second)

    if err := cmd.Start(); err != nil {
        return "", fmt.Errorf("Browser Start Error: %v\n", err)
    }

    var rr RedirectResult

    select {
    case rr = <-redirectResult:
    case <-tch:
        return "", fmt.Errorf("Timeout: waiting redirect.")
    }

    if rr.Err != nil {
        return "", fmt.Errorf("Redirect Error: %v\n", rr.Err)
    }

    fmt.Printf("Got code.\n")

    return rr.Code, nil
}

type RedirectResult struct {
    Code string
    Err  error
}

type LocalServerConfig struct {
    Port    int
    Timeout int
}

ポイントとなるのは、調査のところでも書きましたがexec.Cmdを使用してブラウザを起動しています。その前に承認ボタン押下後のRedirectを受けるためにgoroutineを使ってローカルでサーバーを立ててます。Ridirectのパラメータから認証コードをローカルサーバーで受けとりレスポンスとしてブラウザを閉じるスクリプトを返しています。ここで取得した認証コードを使用して処理を継続しています。
認証コードを受け取るまではchannelを使ってメインスレッドをブロックしています。
ただこのままでは承認ボタンを押さずにブラウザを閉じられた場合に処理が止まったままになるので、タイムアウトを設定して一定時間待ってもRedirectがなかった場合は終了するようにしています。
詳しくはソースコードを読んでもらうほうが早いと思います。

まとめ

やったできたぞ!!!と思っていたのですが、実はリファレンス実装(?)がありました。
https://code.google.com/p/google-api-go-client/source/browse/examples/main.go
なんだあったのかと思ってソースコードを読んでみましたが、細かな部分に違いはありますが、私が思いついた方法と同じような方法でした。


最後の最後にひっくり返されましたが、今までGolang Cafeで学んできた知識(+α)でここまで実装できたので多少はGoの力がついたのかなと。またGolang Cafeも大変いい勉強になっているのではないかと、これからもできるだけ継続して参加していきたいと思います。