Microsoft Azureを利用するデスクトップアプリのデバイス認証


# 概要
[Go から Microsoft Azure を利用する](http://qiita.com/jkawamoto/items/e0761cbe7a4c42305cc0)場合，アクセストークン使用する．
このアクセストークンの取得には複数の方法が用意されているが，
CLI コマンドでも利用されているデバイスコードを用いた認証方法についてまとめる．

# アプリケーションの登録 
認証プロセスで必要となるクライアント ID を取得するために，
Azure ポータルにてアプリケーションの登録を行う．

![1.png](https://qiita-image-store.s3.amazonaws.com/0/62253/ab848f92-cf88-ca27-3ec3-161710e56bc9.png "1.png")

アプリの登録は，ポータルのセキュリティ + ID カテゴリにある「アプリの登録」から行える．

![2.png](https://qiita-image-store.s3.amazonaws.com/0/62253/74546f9d-7e2c-a346-b343-4973439f1ff2.png "2.png")

今回作成するのは，デスクトップアプリケーションなので，アプリの種類としてネイティブを選ぶ．
リダイレクト URL は文字列として有効な URL であれば適当で良いので，
例えば，`http://localhost:18230` などとしておく．



# デバイスコードの取得
デバイスコードの取得には，
https://login.microsoftonline.com/common/oauth2/devicecode
にクライアント ID とアクセスを要求するリソースをクエリとして追加し問い合わせることで取得できる．
レスポンスは次のような構造を持つ JSON 形式で取得できる．

```go
type DeviceCode struct {
	UserCode        string `json:"user_code"`
	DeviceCode      string `json:"device_code"`
	VerificationURL string `json:"verification_url"`
	ExpiresIn       string `json:"expires_in"`
	Interval        string `json:"interval"`
	Message         string `json:"message"`
}
```

次の関数はデバイスコードを取得し上記の構造体として返す．


```go
import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"

	"golang.org/x/net/context/ctxhttp"
)


func GetDeviceCode(ctx context.Context, clientID string) (code *DeviceCode, err error) {

	u := fmt.Sprintf(
		"https://login.microsoftonline.com/common/oauth2/devicecode?client_id=%v&resource=%v",
		clientID,
		url.QueryEscape("https://management.core.windows.net/"))

	req, err := http.NewRequest("Get", u, nil)
	if err != nil {
		return
	}
	req.Header.Add("Accept", "application/json")

	res, err := ctxhttp.Do(ctx, nil, req)
	if err != nil {
		return
	}
	defer res.Body.Close()

	code = new(DeviceCode)
	err = json.NewDecoder(res.Body).Decode(&code)
	return

}
```

# アクセストークンの取得
次に，ユーザにこのデバイスコードにある `VerificationURL` へアクセスしてもらい，
`UserCode` を入力してもらう必要がある．
このユーザによる認証操作が終了すると，
https://login.microsoftonline.com/common/oauth2/token
からアクセストークンを取得できるようになる．

したがって，上記 URL のポーリングを行いユーザの操作を待つ．
ポーリング間隔は `Interval` で指定されている秒数を使う．
なお，このリクエストにはクライアント ID とデバイスコードを含める必要があり，
GET ではなく POST で問い合わせる必要がある．

ユーザがアクセスを承認すると下記の構造体からなる JSON オブジェクトが返ってくる．

```go
type Token struct {
	AccessToken  string `json:"access_token"`
	TokenType    string `json:"token_type"`
	ExpiresIn    string `json:"expires_in"`
	ExpiresOn    string `json:"expires_on"`
	Resource     string `json:"resource"`
	Scope        string `json:"scope"`
	RefreshToken string `json:"refresh_token"`
	IDToken      string `json:"id_token"`
}
```

一方，ユーザが認証を却下した場合は，次の構造体からなる JSON オブジェクトが返ってくる．

```go
type TokenError struct {
	ErrorSummary     string `json:"error"`
	ErrorDescription string `json:"error_description"`
	ErrorCodes       []int  `json:"error_codes"`
	Timestamp        string `json:"timestamp"`
	TraceID          string `json:"trace_id"`
	CorrelationID    string `json:"correlation_id"`
}

// error として使えるように Error() string を定義しておく
func (e *TokenError) Error() string {
	return e.ErrorDescription
}
```


次の関数はデバイスコードを取得し，ユーザの操作が終わるまでポーリングを行いアクセストークンを取得する．

```go
func AuthorizeDeviceCode(ctx context.Context, clientID string) (token *Token, err error) {

	// デバイスコードの取得
	code, err := GetDeviceCode(ctx, clientID)
	if err != nil {
		return
	}

	// ユーザへ認証 URL へのアクセスと UserCode の入力を促すメッセージを表示
	fmt.Println(code.Message)

	// ExpiresIn はデバイスコードの有効期限（秒）
	expire, err := strconv.Atoi(code.ExpiresIn)
	if err != nil {
		return
	}
	interval, err := strconv.Atoi(code.Interval)
	if err != nil {
		return
	}

	// デバイスコードの有効期限が切れる前に以降の操作をキャンセルさせる
	ctx, cancel := context.WithDeadline(
		ctx, time.Now().Add(time.Duration(expire-30)*time.Second))
	defer cancel()

	// ユーザの操作が終わるまでポーリングする
	var req *http.Request
	var res *http.Response
	for {

		body := fmt.Sprintf(
			"resource=%v&client_id=%v&grant_type=device_code&code=%v",
			url.QueryEscape("https://management.core.windows.net/"),
			clientID,
			code.DeviceCode)
		req, err = http.NewRequest("Post", "https://login.microsoftonline.com/common/oauth2/token", strings.NewReader(body))
		if err != nil {
			return
		}
		req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
		req.Header.Add("Accept", "application/json")

		res, err = ctxhttp.Do(ctx, nil, req)
		if err != nil {
			break
		}

		if res.StatusCode == 400 {
			// ユーザが却下した場合はエラーを返す
			var autherror TokenError
			err = json.NewDecoder(res.Body).Decode(&autherror)
			res.Body.Close()
			if err != nil {
				break
			}
			if strings.ToLower(autherror.ErrorSummary) != "authorization_pending" {
				break
			}

		} else {
			// ユーザが承認しアクセストークンが返ってきた場合
			token = new(Token)
			err = json.NewDecoder(res.Body).Decode(token)
			res.Body.Close()
			if err != nil {
				return
			}
			break
		}

		select {
		case <-ctx.Done():
			// デバイスコードの有効期限が切れた場合
			// または上流のコンテキストがキャンセルされた場合
			err = ctx.Err()
			break
		case <-time.After(time.Duration(interval) * time.Second):
		}

	}

	return

}

```

# アクセストークンの更新
得られたトークンには，`ExpiresIn` や `ExpiresOn` という属性があるように，
有効期限が定められている．
有効期限が切れた場合，`RefreshToken` を用いて新しいトークンを取得する．

なお，アクセストークンを更新する場合，ユーザのテナント ID (Active Directory のディレクトリ ID) が必要になる．

```go
func RefreshToken(token *Token, clientID, tenantID string) (newToken *Token, err error) {

	request := make(url.Values)
	request.Add("grant_type", "refresh_token")
	request.Add("client_id", clientID)
	request.Add("refresh_token", token.RefreshToken)
	request.Add("resource", "https://management.core.windows.net/")

	res, err := http.PostForm(fmt.Sprintf(tokenEndpoint, tenantID), request)
	if err != nil {
		return
	}
	defer res.Body.Close()

	if res.StatusCode != 200 {
		var info TokenError
		err = json.NewDecoder(res.Body).Decode(&info)
		if err != nil{
			return nil, err
		}
		return nil, &info
	}

	newToken = new(Token)
	err = json.NewDecoder(res.Body).Decode(newToken)
	return

}
```