Google App Engine(Go)を使ってAWSのマネージメントコンソールへGoogle AppsのID/PasswordでSSOする


今回は[AWS STS](http://docs.aws.amazon.com/STS/latest/APIReference/Welcome.html)のFederation Loginを利用して、Google App Engine(Go)から、Google AppsのID/Passを利用してAWSへSSOする仕組の話です。

# コード

[ここ](https://github.com/soundTricker/aws-federated-login-4-gae-go)にあります。
設定すれば多分どこでも使えますが、
IAMのポリシーがベタ書きです。（めんどかった）

# 経緯

先日土曜日を使って[弊社](http://qiita.com/organizations/bft)の全エンジニア200名ぐらいを集めて、AWSの勉強会をしました。

色々あってEC2の初心者ハンズオンの講師をしなければならなくて、
約150名ぐらいの人たちをAWSのマネージメントコンソールにログインさせる必要が出てきました。

ここで色々問題が有ります。

* クレジットカードを持ってない人がいる可能性もあるためAWSアカウントを作ってもらう事は不可
  * 無料枠との兼ね合いで自分のタイミングで作りたいってのも有るだろうし。
  * 当日作るのは時間的にも無理
  * プリペイド式クレカを配るのはコストが高過ぎる
* IAMユーザを全員分作るってものできるけど、ID/Passwordを発行した場合に、それを全員に配布 & 当日利用してもらうのは混乱を招く可能性がある。
  * 絶対忘れる。 
  * 漏れた時のセキュリティ懸念



弊社ではGoogle Appsを利用していてます。
多分そのID/Passwordを忘れている人はいないと思われる。
そこでAWSからSTSを利用してFederation Loginをさせることにしました。

なお、Google Apps Scriptでも同じ仕組を作れます。
というか作った人がいます。

https://github.com/nazoking/aws-federation-goolge-apps-script

ただGASではFederation Login用URL作成後直接マネージメントコンソールへログイン出来ないので、今回はまじめにappengine上で作ることにしました。

# 問題

色々と...

1. AWS SDKもあるのでJavaでやろうと思いましたがappengine上ではAWSの公式SDKは使えない
2. GoでAWSの公式SDK無い
3. Goでサードパーティ製のAWS ライブラリあるけどSTS対応してない
  * 最初バグってて他の人が出しているPRを取り込んでなかった...(白目
  * やってくれるのは認証部分のみでパラメータ用のXMLは自分で作る必要がある...(白目


でなんだかんだ出来上がりました。

# 技術的には...

特段面白いところは無いと思います。
Goに関してはXMLの扱いが楽だったぐらいですねぇ

```go:sts.go
type GetFederationTokenResponse struct {
	RequestId   string      `xml:"ResponseMetadata>RequestId"`
	Credentials Credentials `xml:"GetFederationTokenResult>Credentials"`
}
type Credentials struct {
	SessionToken    string
	SecretAccessKey string
	Expiration      time.Time
	AccessKeyId     string
}
```

上記のようにタグを利用して定義します。

Google側の認証はappengineに任せています。
つまり何も作っていません。
appengineを認証必須でドメイン限定にする設定とかは他の記事を見ると良いと思います。

またAWS側はSTSで以下の2段階のAPIが必要です
* [GetFederationToken](http://docs.aws.amazon.com/STS/latest/APIReference/API_GetFederationToken.html)
* [GetSigninToken](http://docs.aws.amazon.com/STS/latest/UsingSTS/STSMgmtConsole-manualURL.html)

一つ目のAPIでFederation用のTokenを取得して、APIを叩くためのTokenを取得します。
この段階でIAMのアクセスポリシーを設定します。
ちなみに今回の勉強会では以下の様な設定です。

* 建てられるEC2はt2のみ
* 作れるEBSは8GB以下(要は8GBになるはず)

実際の環境ではこれにさらにTokyoリージョン限定なども含まれています。

そして、二つ目のAPIでマネージメントコンソールへのログイン用トークンを取得します。

このSTS周りのフルコードは下の方に乗っけておきます。

# まとめ

実際勉強会自体でこのマネージメントコンソールへのログインで問題は発生しませんでした。
また後々書きますが、Cloud Trailで全員分の操作ログも取ることができたので非常に良かったです。


Google Appsを使っている企業で、ユーザにマネージメントコンソールを触らせる場合は有りかもしれません。


# コード

```go:sts.go
package main

import (
	"github.com/bridger/aws4"
	"time"
	"net/url"
	"encoding/xml"
	"strconv"
	"encoding/json"
	"fmt"
)

const (
	signinUrl = "https://signin.aws.amazon.com/federation"
)

type GetFederationTokenResponse struct {
	RequestId   string      `xml:"ResponseMetadata>RequestId"`
	Credentials Credentials `xml:"GetFederationTokenResult>Credentials"`
}
type Credentials struct {
	SessionToken    string
	SecretAccessKey string
	Expiration      time.Time
	AccessKeyId     string
}

type SigninToken struct {
	SigninToken string
}

type Session struct {
	SessionId    string `json:"sessionId"`
	SessionToken string `json:"sessionToken"`
	SessionKey   string `json:"sessionKey"`
}


const (
	DEFAULT_DURATION_SECONDS = 43200
	DEFAULT_DESTINATION_URL = "https://console.aws.amazon.com/console/home"
)

type Sts struct {
	Client *aws4.Client
}

func (t *Sts) GetFederationToken(username , policy string, durationSeconds int) (result *GetFederationTokenResponse, err error) {

	vals := make(url.Values)

	vals.Set("Version", "2011-06-15")
	vals.Set("Action", "GetFederationToken")
	vals.Set("Name", username)
	vals.Set("Policy", policy)
	vals.Set("DurationSeconds", strconv.Itoa(durationSeconds))

	res, err := t.Client.PostForm("https://sts.amazonaws.com/", vals)

	if err != nil {
		return
	}

	result = new(GetFederationTokenResponse)
	if err = xml.NewDecoder(res.Body).Decode(&result); err != nil {
		return
	}

	return
}

func (t *Sts) GetSigninToken(session *Session) (result *SigninToken, err error){
	sessionJson, err := json.Marshal(session)

	if err != nil {
		return
	}

	vals := make(url.Values)

	vals.Set("Action", "getSigninToken")
	vals.Set("SessionType", "json")
	vals.Set("Session", string(sessionJson))

	res, err := t.Client.Get(signinUrl + "?" + vals.Encode())

	if err != nil {
		return
	}

	result = new(SigninToken)
	if err = json.NewDecoder(res.Body).Decode(&result); err != nil {
		return
	}
	return
}

func (t *Sts) GenerateFederatedLoginUrl(signinToken *SigninToken, issuer, destination string) string {
	vals := make(url.Values)

	vals.Set("Action", "login")
	vals.Set("SigninToken", signinToken.SigninToken)
	vals.Set("Issuer", issuer)
	vals.Set("Destination", destination)

	return fmt.Sprintf("%s?%s", signinUrl, vals.Encode())
}
```