🙆

【Go/AWS】Cognitoのダミーユーザーを作るスクリプトを書いてみた

2021/07/05に公開

開発環境用にCognitoのダミーユーザーのAccessTokenが欲しいことが多々ある。
一回一回UIからユーザーを作成するのが面倒だったので、Goでスクリプトを書いてみた。

動作環境

  • macOS Catalina 10.17.7
  • go1.16.5

Cognitoの設定

  • メールアドレスをユーザ名として使用
  • アプリクライアントの認証フローはALLOW_ADMIN_USER_PASSWORD_AUTHのみを選択
  • 認証にはクライアントID、クライアントシークレットを使用
  • パスワードは数字、特殊文字、大文字、小文字で構成されていることが必須
  • 管理者のみにユーザーの作成を許可

ソースコード

package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"flag"
	"fmt"
	"os"
	"time"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/cognitoidentityprovider"
)

var (
	poolID       = "xxxxxxxxxxxxxxxxxxxxx"
	clientID     = "xxxxxxxxxxxxxxxxxxxxxxxxxx"
	clientSecret = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
)

func main() {
	sess := session.Must(session.NewSessionWithOptions(session.Options{
		SharedConfigState: session.SharedConfigEnable,
	}))
	cognitoClient := cognitoidentityprovider.New(sess)

	userName := flag.String("u", "", "user name")
	password := flag.String("p", "", "password")
	flag.Parse()

	if *userName == "" || *password == ""{
		fmt.Println("ivalid parameter")
		os.Exit(1)
	}

	newUserData := &cognitoidentityprovider.AdminCreateUserInput{
		UserPoolId:        &poolID,
		Username:          userName,
		TemporaryPassword: password,
	}

	_, err := cognitoClient.AdminCreateUser(newUserData)
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

	mac := hmac.New(sha256.New, []byte(clientSecret))
	mac.Write([]byte(*userName + clientID))
	secretHash := base64.StdEncoding.EncodeToString(mac.Sum(nil))

	initiateAuthOutput, err := cognitoClient.AdminInitiateAuth(&cognitoidentityprovider.AdminInitiateAuthInput{
		AuthFlow:   aws.String("ADMIN_USER_PASSWORD_AUTH"),
		UserPoolId: &poolID,
		ClientId:   &clientID,
		AuthParameters: map[string]*string{
			"USERNAME":    userName,
			"PASSWORD":    password,
			"SECRET_HASH": &secretHash,
		},
	})
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

	respondToAuthChallengeOutput, err := cognitoClient.AdminRespondToAuthChallenge(&cognitoidentityprovider.AdminRespondToAuthChallengeInput{
		UserPoolId:    &poolID,
		ClientId:      &clientID,
		ChallengeName: aws.String("NEW_PASSWORD_REQUIRED"),
		ChallengeResponses: map[string]*string{
			"USERNAME":     userName,
			"NEW_PASSWORD": password,
			"SECRET_HASH":  &secretHash,
		},
		Session: initiateAuthOutput.Session,
	})
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

	fmt.Println(*respondToAuthChallengeOutput.AuthenticationResult.AccessToken)
}

全体の流れとしては、
新規ユーザー作成

認証フローの開始

ユーザーのステータスを更新しAccessTokenを取得
となっている。
なお、今回使用するAPIは全て管理者APIである。

少し細かく解説する。

ユーザー新規作成

ユーザー名とパスワードをコマンドライン引数で受け取り、新規ユーザーを作成するようにしている。
ダミーのユーザーのパスワードなのでそこまでセキュアに扱わなくても良いだろうということで、パスワードの入力を非表示にはしない。

	userName := flag.String("u", "", "user name")
	password := flag.String("p", "", "password")
	flag.Parse()

	if *userName == "" || *password == ""{
		fmt.Println("ivalid parameter")
		os.Exit(1)
	}

	newUserData := &cognitoidentityprovider.AdminCreateUserInput{
		UserPoolId:        &poolID,
		Username:          userName,
		TemporaryPassword: password,
	}

	_, err := cognitoClient.AdminCreateUser(newUserData)
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

認証フローの開始

上記で新規作成したユーザーの状態はFORCE_CHANGE_PASSWORDとなっている。
このままだとAccessTokenは取得できない。
認証フローを開始して初期パスワードを更新し、状態をCONFIRMEDにすることでAccessTokenが取得できるようになる。
認証フローの開始と、パスワードの更新は別のAPIとなっているのでまずは認証フロー開始APIを使用しリクエストを送信する。
認証フロー開始APIのリクエストには、ユーザー名、クライアントID、クライアントシークレットから生成したシークレットハッシュを含める必要がある。(クライアントシークレットを生成していない場合はおそらく不要)

	mac := hmac.New(sha256.New, []byte(clientSecret))
	mac.Write([]byte(*userName + clientID))
	secretHash := base64.StdEncoding.EncodeToString(mac.Sum(nil))

	initiateAuthOutput, err := cognitoClient.AdminInitiateAuth(&cognitoidentityprovider.AdminInitiateAuthInput{
		AuthFlow:   aws.String("ADMIN_USER_PASSWORD_AUTH"),
		UserPoolId: &poolID,
		ClientId:   &clientID,
		AuthParameters: map[string]*string{
			"USERNAME":    userName,
			"PASSWORD":    password,
			"SECRET_HASH": &secretHash,
		},
	})
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

ユーザーのステータスを更新しAccessTokenを取得

認証フロー開始のリクエストが成功した場合のレスポンスに含まれるsessionを利用し、新しいパスワードを登録する。
なお、この際のパスワードは初期パスワードと全一致していても問題ない。(ちょっと気持ち悪いがダミーなので良しとする)
無事リクエストが成功すれば、レスポンスには御目当てのAccessTokenが含まれている。

	respondToAuthChallengeOutput, err := cognitoClient.AdminRespondToAuthChallenge(&cognitoidentityprovider.AdminRespondToAuthChallengeInput{
		UserPoolId:    &poolID,
		ClientId:      &clientID,
		ChallengeName: aws.String("NEW_PASSWORD_REQUIRED"),
		ChallengeResponses: map[string]*string{
			"USERNAME":     userName,
			"NEW_PASSWORD": password,
			"SECRET_HASH":  &secretHash,
		},
		Session: initiateAuthOutput.Session,
	})
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

	fmt.Println(*respondToAuthChallengeOutput.AuthenticationResult.AccessToken)

さいごに

楽する為にさくっとスクリプトを作ろうと試みたが、Cognitoの仕様が全然分かっていなかった為思ったよりも大変だった。
Cognitoについてほんの少し詳しくなれたので結果オーライとする。

参考

https://dev.to/mcharytoniuk/using-aws-cognito-app-client-secret-hash-with-go-8ld
https://dev.classmethod.jp/articles/change-cognito-user-force_change_passwore-to-confirmed/
https://www.wakuwakubank.com/posts/696-aws-cognito/

Discussion