Open4

cognito

プログラマぽい何かを名乗る生物プログラマぽい何かを名乗る生物

設定ログ

または SAML もしくは Open ID Connect を介して外部ディレクトリの認証情報を使用して、サインインできます。ユーザープールのフェデレーティッドユーザーのユーザー属性マッピングとセキュリティを管理できます。

今回、saml,oidcをやりたいのでそうした

電話番号はなんか好きじゃない

パスワードポリシー
→ 有効期限がデフォルトだと7日だが、3日にした。なんか長い。

多要素認証

  • MFA必須だとユーザーが離脱しそうなので一旦やめた
  • Authenticatorアプリケーションはよく使うので選択した
  • smsメッセージもよくあるパターンなので選択した
  • ユーザーアカウントの復旧については使用できる場合はEメール、それ以外の場合はSMSにした

サインアップエクスペリエンス

  • 自己登録は有効化しない BtoB saasでそれはやっちゃいけないと思ってる。
  • 属性検証とユーザーアカウントの確認は画面の通り。と言うかデフォルトを利用した。
  • カスタム属性でtenantを追加



Eメール送信については、プロダクションではないのでやめておいた

smsのロールはわかりやすい名前で新規作成した

cognitoのクライアントを使用せずにカスタムしてできるよう作ってみる

https://dcj71ciaiav4i.cloudfront.net/591796E0-D127-11EB-A6A5-FB83B2BAF6EE/chapter1.html

ココ迷うな。
クライアントシークレットは作成するけど。

ここら辺は改めてoidcの仕様書読まないとな・・・。

プログラマぽい何かを名乗る生物プログラマぽい何かを名乗る生物

cognito with aws sdk

今回、user poolを作ったのでそれを用いた認証フローを作る

ハンズオン内容

SignUp

ブラウザやモバイルアプリから呼び出されることが前提

{
"ClientId":"string", // Cognitoユーザープールに登録したアプリクライアントのID
"SecretHash": "string", // シークレットを設定した場合に必要 つまり上記設定だと必要
"UserName":"string", 
"Password":"string"
}

ConfirmSignUp

SignUpしてもまだ仮登録状態。
ユーザー自身がメールやSMAで受け取った確認コードをAPIで送る必要がある。
もしくはAdminConfirmSiguUpで管理者が認証する必要がある。

  • ClientId
  • SecretHash
  • Username
  • ConfirmationCode

InitiateAuth

ユーザー認証を行うAPI
使用する認証フロー、認証にMFAを利用するかで大きくAPIの利用方法が変わる。

リクエストボディ

{
   "ClientId": "string",
   "AuthFlow": "USER_PASSWORD_AUTH",
   "AuthParameters": { 
      "USERNAME" : "string",
      "PASSWORD" : "string"
    }
}

推定レベルだがSRP_AUTHのボディ

↓ 参照元
https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_InitiateAuth.html#API_InitiateAuth_RequestSyntax

{
    "AuthFlow": "USER_SRP_AUTH",
    "ClientId": "1example23456789", 
    "AuthParameters": {
        "USERNAME": "exampleuser",
        "SRP_A": "exampleSrpValue",
        "SECRET_HASH": "oT5ZkS8ctnrhYeeGsGTvOzPhoc/Jd1cO5fueBWFVmp8="
    },
    "AnalyticsMetadata": {
        "AnalyticsEndpointId": "exampleAnalyticsEndpointId"
    },
    "UserContextData": {
        "EncodedData": "AmazonCognitoAdvancedSecurityData_object",
        "IpAddress": "192.0.2.1"
    },
    "ClientMetadata": {
        "CustomKey": "CustomValue"
    }
}

RespondToAuth

{
    "ChallengeName": "PASSWORD_VERIFIER",
    "ChallengeResponses": {
        "USERNAME": "exampleuser",
        "PASSWORD_CLAIM_SIGNATURE": "exampleClaimSignature",
        "PASSWORD_CLAIM_SECRET_BLOCK": "exampleSecretBlock",
        "TIMESTAMP": "2024-11-17T12:34:56Z"
    },
    "ClientId": "1example23456789",
    "Session": "exampleSessionId"
}

DeleteUser

{
   "AccessToken": "string"
}

APIGatewayでCORS設定

リソース画面からメソッドレスポンスを指定

プログラマぽい何かを名乗る生物プログラマぽい何かを名乗る生物

go sdkを用いたcognitoの認証について

今回、自分でサインアップできないように作ったので、ユーザー作成で利用するAPIはSignUpではなくAdminCreateUserになる。(と思ってる)

https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_AdminCreateUser.html

input := &cognitoidentityprovider.AdminCreateUserInput{
		UserPoolId: aws.String("ap-northeast-1_XXXX"), // ユーザープールID
		Username:   aws.String("testuser"),         // ユーザー名
		DesiredDeliveryMediums: []types.DeliveryMediumType{
			types.DeliveryMediumTypeSms, // SMSで通知
		},
		MessageAction: types.MessageActionTypeSuppress, // 通知を抑制
		TemporaryPassword: aws.String("This-is-my-test-99!"), // 一時パスワード
		UserAttributes: []types.AttributeType{
			{
				Name:  aws.String("name"),
				Value: aws.String("John"),
			},
			{
				Name:  aws.String("phone_number"),
				Value: aws.String("+12065551212"),
			},
			{
				Name:  aws.String("email"),
				Value: aws.String("testuser@example.com"),
			},
		},
	}

	// リクエスト送信
	return c.AdminCreateUser(context.TODO(), input)

これで一応作られる。認証しようとする場合はこう。

func calculateSecretHash(username, clientId, clientSecret string) string {
	data := username + clientId
	h := hmac.New(sha256.New, []byte(clientSecret))
	h.Write([]byte(data))
	return base64.StdEncoding.EncodeToString(h.Sum(nil))
}

func AuthorizeWithPassword(c *cognitoidentityprovider.Client) (*cognitoidentityprovider.InitiateAuthOutput, error) {
	username := "testuser"
	clientId := "client id"
	clientSecret := "secret"
	secretHash := calculateSecretHash(username, clientId, clientSecret)
	input := &cognitoidentityprovider.InitiateAuthInput{
		AuthFlow:       types.AuthFlowTypeUserPasswordAuth,
		ClientId: 		 aws.String("client id"),
		AuthParameters: map[string]string{
			"USERNAME": "testuser",
			"PASSWORD": "This-is-my-test-99!!!",
			"SECRET_HASH": secretHash,
		},
	}

	ret, err := c.InitiateAuth(context.TODO(), input)

	if err != nil {
		fmt.Println("Error")
		fmt.Println(err)
		return nil, err
	}

	if ret.ChallengeName == types.ChallengeNameTypeNewPasswordRequired {
		fmt.Println("New password required")
		_, err := RespondToNewPasswordChallenge(c, secretHash, ret.Session)
		if err != nil {
			fmt.Println("Error")
			fmt.Println(err)
			return nil, err
		}
	} else {
		fmt.Println("No new password required")
	}

	return ret, nil
}

func RespondToNewPasswordChallenge(c *cognitoidentityprovider.Client, hash string, session *string) (*cognitoidentityprovider.RespondToAuthChallengeOutput, error) {
	input := &cognitoidentityprovider.RespondToAuthChallengeInput{
		ChallengeName: types.ChallengeNameTypeNewPasswordRequired,
		ClientId: aws.String("client id"),
		ChallengeResponses: map[string]string{
			"USERNAME": "testuser",
			"NEW_PASSWORD": "This-is-my-test-99!!!",
			"SECRET_HASH": hash,
		},
		Session: session,
	}

	ret, err := c.RespondToAuthChallenge(context.TODO(), input)

	if err != nil {
		fmt.Println("Error")
		fmt.Println(err)
		return nil, err
	}

	return ret, nil

}

calculateSecretHashはこう計算する式が元になっている。

SECRET_HASH = Base64( HMAC-SHA256( ClientSecret, Username + ClientId ) )
  • ClientSecret: Cognitoアプリクライアントのシークレットキー。
  • Username: 認証に使用するユーザー名。
  • ClientId: CognitoアプリクライアントのID。

基本的に・・・

- AdminCreateUser
- InitiateAuth
  - ChallengeName:"NEW_PASSWORD_REQUIRED"
- RespondToAuthChallenge
- InitiateAuth
プログラマぽい何かを名乗る生物プログラマぽい何かを名乗る生物

SRPってなんだ・・・?

  • パスワード認証のための認証キー交換プロトコル

特徴

  • パスワードをサーバに送信しない
  • サーバ側にパスワードのハッシュ値を保存しない
  • 秘密鍵は全て1回限りのランダム値

登録→認証 の流れをとる

登録

クライアントで

  • ランダムなソルト・検証子を生成
  • パスワードとソルトをハッシュ化した値を送信

検証子を用いると、サーバ側でパスワードを保存することなく、パスワードの正当性を検証できる。
検証子は不可逆であり検証子から元のパスワードを復元することはできない。

認証

  • クライアント側でランダムな秘密鍵aとそれから生成される効果鍵Aを生成する
  • クライアントはユーザー名とともに公開鍵Aをサーバに送信する

サーバ側でも同様にランダムな秘密鍵bと検証子vから生成される公開鍵Bを生成する。

その後、サーバーは検証子とソルトが保管されているデータベースからソルトを取得し、ソルトと公開鍵Bをクライアントに送信する。

クライアントとサーバーはそれぞれの公開鍵AとBを用いて共有鍵を生成し、それを用いてセッションキーを生成する。

クライアントはユーザー名、パスワード、共有鍵、ソルトを用いてセッションキーを生成し、それをハッシュ化したものをサーバに送信する。

サーバーは検証子、ソルト、共有鍵を用いてセッションキーを生成する。

最後に、サーバーでセッションキーが一致するか確認し、検証結果をクライアントに送信する。

ーーーーこれを写経しただけーーーー
https://zenn.dev/caru/articles/ec27e2d1700270