golangで動かして学ぶCognitoユーザプール、IDプール

# Cognitoをgolnagから操作してみる

イマイチ理解しきれてなかったので、ラッパーライブラリを作りつつ、勉強してみました。
元ネタはhttps://dev.classmethod.jp/articles/get-aws-temporary-security-credentials-with-cognito-id-pool-by-aws-cli
で、AWS CLIベースの記事だったのをgolangに書き起こしつつ理解するって形を取りました。

作ったラッパーは
https://gitlab.com/kuritayu/cognito
におきました。

## ユーザプール、IDプールの違い

1. ユーザプール: ユーザ情報、ログイン情報の管理(認証担当)
1. IDプール: ユーザに応じた一時クレデンシャルキーの発行(認可担当)

## パターン別のクレデンシャルキーの発行

1. 認証済・非認証に応じたクレデンシャルキーの発行(IAM Role)
1. ルールベースのクレデンシャルキーの発行
1. ユーザプールに設定したIAM Roleでクレデンシャルキーを発行

## 環境構築

上記クラスメソッドさんの記事に詳細に記載されているため、割愛

## 認証部品(ユーザプール操作)

### 初期化

セッション情報、プールID、アプリクライアントIDを引数に、構造体を構築します。

```golang
	region := "region名"
	poolId := "ユーザプールID"
	clientId := "アプリクライアントID"
	sess := session.Must(
		session.NewSession(&aws.Config{
			Region: aws.String(region),
		}))
	userPool := New(sess, poolId, clientId)
```

### ユーザ作成

saveメソッドを呼び出すだけです。
今回のユーザプールでは必須属性としてメールアドレス、カスタム属性として会社名を入れてみました。

```golang
func TestUserPool_Save(t *testing.T) {
	err := userPool.Save("ユーザ名", "メールアドレス", "会社名")
	assert.NoError(t, err)
}
```

saveメソッドでは、AttributeTypeにemailやカスタム属性の会社名を設定しています。

```golang
func (p UserPool) Save(name string, email string, company string) error {

	var attrs []*cognitoidentityprovider.AttributeType
	emailAttr := p.userAttribute(cognitoidentityprovider.UsernameAttributeTypeEmail, email)
	attrs = append(attrs, emailAttr)
	companyAttr := p.userAttribute("custom:company", company)
	attrs = append(attrs, companyAttr)

	input := &cognitoidentityprovider.AdminCreateUserInput{
		UserPoolId:     aws.String(p.PoolId),
		Username:       aws.String(name),
		UserAttributes: attrs,
	}

	_, err := p.client.AdminCreateUser(input)
	if err != nil {
		return err
	}

	return nil
}
```

### ユーザ情報取得

ユーザ名を引数にとり、構造体で返します。

```golang
func TestUserPool_FindOne(t *testing.T) {
	out, err := userPool.FindOne("ユーザ名")
	assert.NoError(t, err)
}
```

FindOneメソッドですが、UserAttributesが配列で入っていて厄介です。
ループで回して構造体に入れていますが、他にいい方法があるのかも。

```golang
func (p UserPool) FindOne(name string) (*UserInfo, error) {

	input := &cognitoidentityprovider.AdminGetUserInput{
		UserPoolId: aws.String(p.PoolId),
		Username:   aws.String(name),
	}

	out, err := p.client.AdminGetUser(input)
	if err != nil {
		return nil, err
	}

	userInfo := &UserInfo{
		Name: name,
	}
	for _, a := range out.UserAttributes {
		switch aws.StringValue(a.Name) {
		case "sub":
			userInfo.Uuid = aws.StringValue(a.Value)
		case "email":
			userInfo.Email = aws.StringValue(a.Value)
		case "custom:company":
			userInfo.Company = aws.StringValue(a.Value)
		}
	}

	return userInfo, nil
}
```

### 認証

こちらもシンプルにID/Passwordを引数にわたすだけです。IDトークンが返ってきます。(IDプールで使うので)

```golang
func TestUserPool_Authenticate(t *testing.T) {
	out, err := userPool.Authenticate("ユーザ名", "パスワード")
	assert.NoError(t, err)
}
```

Authenticateメソッドは、凝ったことはしていません。

```golang
func (p UserPool) Authenticate(name string, password string) (string, error) {

	input := &cognitoidentityprovider.AdminInitiateAuthInput{
		AuthFlow: aws.String("ADMIN_USER_PASSWORD_AUTH"),
		AuthParameters: map[string]*string{
			"USERNAME": aws.String(name),
			"PASSWORD": aws.String(password),
		},
		ClientId:   aws.String(p.clientId),
		UserPoolId: aws.String(p.PoolId),
	}

	out, err := p.client.AdminInitiateAuth(input)
	if err != nil {
		return "", err
	}

	return aws.StringValue(out.AuthenticationResult.IdToken), nil
}
```

## 認可部品(IDプール操作)

### 初期化

セッション情報、プールIDを引数に、構造体を構築します。

```golang
	region := "リージョン名"
	poolId := "IDプール名"
	sess := session.Must(
		session.NewSession(&aws.Config{
			Region: aws.String(region),
		}))
	idPool := New(sess, poolId)
```

### Identity ID取得

認証済、非認証で2パターン用意しました。
ユーザプールから取得したIDトークンを使うかどうか、の違いですね。

```golang
func TestIdPool_GetIdWithoutAuth(t *testing.T) {
	out, err := idPool.GetIdWithoutAuth()
	assert.NoError(t, err)
}

func TestIdPool_GetIdWithAuth(t *testing.T) {
	userPool := SetUpForUserPool()
	idToken, err := userPool.Authenticate("ユーザ名", "パスワード")
	assert.NoError(t, err)

	identityId, err := idPool.GetIdWithAuth(idToken, userPool.PoolId)
	assert.NoError(t, err)
}
```

メソッドとしては、インプットのパラメータが違うだけです。
投げるリクエストは共通化しています。

```golang
func (i IdPool) GetIdWithoutAuth() (string, error) {

	input := &cognitoidentity.GetIdInput{
		IdentityPoolId: aws.String(i.poolId),
	}

	return i.doGetId(input)
}


func (i IdPool) GetIdWithAuth(token string, userPoolId string) (string, error) {

	param := make(map[string]*string)
	provider := fmt.Sprintf("cognito-idp.%v.amazonaws.com/%v", aws.StringValue(i.client.Config.Region), userPoolId)
	param[provider] = aws.String(token)

	input := &cognitoidentity.GetIdInput{
		IdentityPoolId: aws.String(i.poolId),
		Logins:         param,
	}

	return i.doGetId(input)
}

func (i IdPool) doGetId(input *cognitoidentity.GetIdInput) (string, error) {

	out, err := i.client.GetId(input)
	if err != nil {
		return "", err
	}

	return aws.StringValue(out.IdentityId), nil
}


```

### 認可(クレデンシャルキー発行)

取得したクレデンシャルキーをstsでチェックし、割り当てられていることまで確認しました。

```golang
func TestIdPool_GetCredentialWithoutAuth(t *testing.T) {
	idPool := SetUpForIdPool()
	identityId, _ := idPool.GetIdWithoutAuth()
	out, err := idPool.GetCredentialWithoutAuth(identityId)
	assert.NoError(t, err)

	sts, err := SetUpSTS(out.accessKey, out.secretKey, out.sessionToken)
	assert.NoError(t, err)

	log.Print(sts)
}

func TestIdPool_GetCredentialWithAuth(t *testing.T) {
	userPool := SetUpForUserPool()
	idToken, err := userPool.Authenticate("ユーザ名", "パスワード")
	assert.NoError(t, err)

	idPool := SetUpForIdPool()
	identityId, err := idPool.GetIdWithAuth(idToken, userPool.PoolId)
	assert.NoError(t, err)

	out, err := idPool.GetCredentialWithAuth(identityId, idToken, userPool.PoolId)
	assert.NoError(t, err)

	sts, err := SetUpSTS(out.accessKey, out.secretKey, out.sessionToken)
	assert.NoError(t, err)

	log.Print(sts)

}
```