GoogleAPIを利用するデスクトップアプリのOAuth2.0認証


# 概要
Google の API を利用するデスクトップアプリが OAuth 2.0 プロトコルを走らせる方法．
Go 標準の[oauth2](https://godoc.org/golang.org/x/oauth2)パッケージはほとんどの処理を実行してくれるが，
認証コードを受け取るために一時的な ローカルWebサーバを立てることと，
Code verifier の計算は自前で行う．


# アプリの登録
まず初めに，[コンソールの認証情報](https://console.cloud.google.com/apis/credentials)ページでアプリのクライアント ID とクライアントシークレットを取得する．

コンソールの API Manager から認証情報を開き，認証情報を作成，OAuth クライアント ID を選択する．
![1.png](https://qiita-image-store.s3.amazonaws.com/0/62253/efb791a6-98bb-420b-fee8-961a70f08d14.png "1.png")

デスクトップアプケーションから使用するため，その他を選び作成する．

![2.png](https://qiita-image-store.s3.amazonaws.com/0/62253/7d0409f7-8790-b95b-1ae7-63b82f58c751.png "2.png")

クライアント ID とクライアントシークレットが表示されるので控えておく．
なお，忘れてしまっても認証情報ページで再確認できる．

![3.png](https://qiita-image-store.s3.amazonaws.com/0/62253/ee06ee43-da32-6a1a-5171-b0b7d4af471c.png "3.png")


# Code verifier の計算
サーバから送られてくる認証コードを横取りされる中間者攻撃を防ぐため，
Code verifier というものを利用できる．
具体的には，認証コードの要求時にランダム文字列(Code verifier)のハッシュ値を送っておき，
アクセストークンを要求する時にはランダム文字列を送る．
認証コードが横取りされてもランダム文字列の方がバレなければ
アクセストークンを不正に取得されることを防げる．

Code verifier は，次の文字からなる長さ43から128の文字列と決められている．

* [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~"

まずは，Code verifier の生成関数を作る．


```go
import (
	"crypto/rand"
	"math/big"
)

var (
	CodeVerifierChars []byte
)

func init() {
	for b := byte('a'); b <= byte('z'); b++ {
		CodeVerifierChars = append(CodeVerifierChars, b)
	}
	for b := byte('A'); b <= byte('Z'); b++ {
		CodeVerifierChars = append(CodeVerifierChars, b)
	}
	for b := byte('0'); b <= byte('9'); b++ {
		CodeVerifierChars = append(CodeVerifierChars, b)
	}
	CodeVerifierChars = append(CodeVerifierChars, byte('-'), byte('.'), byte('_'), byte('~'))
}

func GenerateCodeVerifier() (codeVerifier []byte, err error) {

	max := big.NewInt(int64(len(CodeVerifierChars)))
	var n *big.Int
	for i := 0; i != 128; i++ {
		n, err = rand.Int(rand.Reader, max)
		if err != nil {
			return
		}
		codeVerifier = append(codeVerifier, CodeVerifierChars[n.Int64()])
	}
	return

}
```
(init の中身はもう少し賢くできる気がする)

乱数生成は [math/rand](https://godoc.org/math/rand) パッケージでもできるが，より安全な [crypto/rand](https://golang.org/pkg/crypto/rand/) の方を使ってみた．

先に述べた通り，認証コードの要求時には生成された Code verifier のハッシュ値(Code challenge)を送る．
正確にはパディング無し Base64 エンコードした SHA256 ハッシュ値を送る．
下記のコードはその Code challenge を計算する．

```go
codeVerifier := GenerateCodeVerifier()
sum := sha256.Sum256(codeVerifier)
codeChallenge := strings.TrimRight(base64.URLEncoding.EncodeToString(sum[:]), "=")
```



# 認証コード受け取り用サーバ
次に，認証コードを受け取るための Web サーバを用意する．
認証コードは，コードと要求時に指定した State の組みで返ってくるため，構造体を定義しておく．

```go
type authorizationCode struct {
	Code  string
	State string
}
```

Web サーバの方は goroutine として動かすので，認証コードとエラーを goroutine 呼び出し側が
受け取れるようにチャンネルを作っておく．

```go
type codeReceiver struct {
	Result chan *authorizationCode
	Error  chan error
}

func newCodeReceiver() *codeReceiver {
	return &codeReceiver{
		Result: make(chan *authorizationCode, 1),
		Error:  make(chan error, 1),
	}
}
```

この Web サーバは，認証コードを受け取る一回のリクエストだけ処理できれば良い．
認証コードあるいはエラー情報はリクエスト URL のクエリとして送られてくるので，
内容に合わせて処理を分ける．

```go
const (
	AuthSucceedURL = "https://jkawamoto.github.io/roadie/auth/succeed/"
	AuthErrorURL = "https://jkawamoto.github.io/roadie/auth/error/"
)

func (r *codeReceiver) ServeHTTP(res http.ResponseWriter, req *http.Request) {

	queries := req.URL.Query()
	if errCode := queries.Get("error"); errCode != "" {
		res.Header().Add("Location", AuthErrorURL)
		r.Error <- fmt.Errorf("Failed authorization: %v", errCode)

	} else if code := queries.Get("code"); code == "" {
		res.Header().Add("Location", AuthErrorURL)
		r.Error <- fmt.Errorf("Failed authorization")

	} else {
		res.Header().Add("Location", AuthSucceedURL)
		r.Result <- &authorizationCode{
			Code:  queries.Get("code"),
			State: queries.Get("state"),
		}

	}
	res.WriteHeader(http.StatusFound)

}
```

今回は，GitHub Pages に認証成功時と失敗時に表示する Web ページを用意しているので，
そちらに転送するレスポンスを返している．

もちろんアプリケーション側で HTML 文書を作成して `res.Write(...)` のように
ResponseWriter 型の `res` に直接書き込んでも良い．

この Web サーバを起動するには，次のようにする．

```go
port := 18029
listener, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%v", port))
if err != nil {
	return
}
defer listener.Close()

receiver := newCodeReceiver()
go http.Serve(listener, receiver)
```

# アクセストークンの要求
以上を合わせてアクセストークンを要求する．
まず，Code verifier, Code challenge を計算し，ローカル Web サーバを起動する．

その後は，[oauth2](https://godoc.org/golang.org/x/oauth2)パッケージの [Config](https://godoc.org/golang.org/x/oauth2#Config) が提供する [AuthCodeURL](https://godoc.org/golang.org/x/oauth2#Config.AuthCodeURL) と [Exchenge](https://godoc.org/golang.org/x/oauth2#Config.Exchange) を使って
アクセストークンを取得する．

Config はクライアント ID, クライアントシークレット，エンドポイント，リダイレクトURL(起動したローカル Web サーバのURL)，そしてスコープを取る．
なおスコープの一覧は，[ここ](https://developers.google.com/identity/protocols/googlescopes)にある．

```go
const (
	authorizeEndpoint = "https://accounts.google.com/o/oauth2/v2/auth"
	tokenEndpoint = "https://www.googleapis.com/oauth2/v4/token"
	gcpScope = "https://www.googleapis.com/auth/cloud-platform"
)

cfg := &oauth2.Config{
	ClientID:     ClientID,
	ClientSecret: ClientSecret,
	Endpoint: oauth2.Endpoint{
		AuthURL:  authorizeEndpoint,
		TokenURL: tokenEndpoint,
	},
	RedirectURL: fmt.Sprintf("http://127.0.0.1:%v", port),
	Scopes:      []string{gcpScope},
}

```

Config を作成したら，AuthCodeURL を使って認証コード要求用 URL を取得する．
この時，State と Code challenge を渡す．

```go
state := fmt.Sprintf("%v", time.Now().Unix())
url := cfg.AuthCodeURL(
	state,
	oauth2.SetAuthURLParam("code_challenge_method", "S256"),
	oauth2.SetAuthURLParam("code_challenge", codeChallenge))
```

ユーザにこの得られた URL にアクセスしてもらい，認証操作を行ってもらう．
ユーザがアクセス許可を与えるか，キャンセルするかどちらかのアクションを行うと，
先ほど作成したサーバから認証コードあるいはエラーがチャンネル経由で送られてくる．

認証コードの場合は State 情報が上記で設定したものと同じか確認してから次へ進む．

```go
var code *authorizationCode
select {
case code = <-receiver.Result:
    if code.State != state {
        err = fmt.Errorf("Received state dosen't match")
        return
    }

case err = <-receiver.Error:
    return

case <-ctx.Done():
    return nil, ctx.Err()

}
```

なお上記のコードでは context のキャンセルも合わせてチェックしている．

最後に，Config の Exchenge を使って認証コードからアクセストークンを取得するのだが，
この時 Code verifier を合わせて送るようにしておく．

```go
cfg.Endpoint.TokenURL = fmt.Sprintf(
	"%v?code_verifier=%v", cfg.Endpoint.TokenURL, string(codeVerifier))

token, err := cfg.Exchange(ctx, code.Code)
```

以上すべてを合わせると次のようになる．

```go
func RequestToken(ctx context.Context) (token *oauth2.Token, err error) {

    // Code verifier, Code challenge の計算
	codeVerifier, err := GenerateCodeVerifier()
	if err != nil {
		return
	}
	sum := sha256.Sum256(codeVerifier)
	codeChallenge := strings.TrimRight(base64.URLEncoding.EncodeToString(sum[:]), "=")

	// Web サーバの起動
	port := 18029
	listener, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%v", port))
	if err != nil {
		return
	}
	defer listener.Close()

	receiver := newCodeReceiver()
	go http.Serve(listener, receiver)

	// Config の準備
	cfg := &oauth2.Config{
		ClientID:     ClientID,
		ClientSecret: ClientSecret,
		Endpoint: oauth2.Endpoint{
			AuthURL:  authorizeEndpoint,
			TokenURL: tokenEndpoint,
		},
		RedirectURL: fmt.Sprintf("http://127.0.0.1:%v", port),
		Scopes:      []string{gcpScope},
	}

	// URL の取得	
	state := fmt.Sprintf("%v", time.Now().Unix())
	url := cfg.AuthCodeURL(
		state,
		oauth2.SetAuthURLParam("code_challenge_method", "S256"),
		oauth2.SetAuthURLParam("code_challenge", codeChallenge))
	
	// URLを表示してユーザにアクセスさせる
	fmt.Println(url)

	// アクセストークンの取得
	var code *authorizationCode
	select {
	case code = <-receiver.Result:
		if code.State != state {
			err = fmt.Errorf("Received state dosen't match")
			return
		}

	case err = <-receiver.Error:
		return
	case <-ctx.Done():
		return nil, ctx.Err()
	}

	cfg.Endpoint.TokenURL = fmt.Sprintf(
		"%v?code_verifier=%v", cfg.Endpoint.TokenURL, string(codeVerifier))
	return cfg.Exchange(ctx, code.Code)

}

```

# アクセストークンの利用
得られたアクセストークンは，そのトークンを使用する http.Client を作成し他の API クライアントに渡すことで利用できる．
トークンを使った http.Client の作成は，oauth2.Config 経由で行う．

```go
ctx := context.Background()
cfg := &oauth2.Config{
	ClientID:     ClientID,
	ClientSecret: ClientSecret,
	Endpoint: oauth2.Endpoint{
		AuthURL:  authorizeEndpoint,
		TokenURL: tokenEndpoint,
	},
	Scopes:      []string{gcpScope},
}

client := cfg.Client(ctx, token)
```

このクライアントを使うと，アクセストークンが期限切れになった場合に自動で再取得してくれる．

このクライアントを使って，例えば利用可能な Zone 一覧を取得する場合，
次のようになる．

```go
import	(
	"context"
	"fmt"

	"google.golang.org/api/compute/v1"
)

func Zone(ctx context.Context, project string, token *oauth2.Token) (err error){

	cfg := &oauth2.Config{
		ClientID:     ClientID,
		ClientSecret: ClientSecret,
		Endpoint: oauth2.Endpoint{
			AuthURL:  authorizeEndpoint,
			TokenURL: tokenEndpoint,
		},
		Scopes:      []string{gcpScope},
	}

	client := cfg.Client(ctx, token)
	service, err := compute.New(client).newService(ctx)
	if err != nil {
		return
	}

	res, err := service.Zones.List(project).Do()
	if err != nil {
		return
	}

	for _, v := range res.Items {
		fmt.Println(v.Name, v.Status)
	}
	return

}
```


# 参考
* [OAuth 2.0 for Mobile & Desktop Apps](https://developers.google.com/identity/protocols/OAuth2InstalledApp)