cognito
設定ログ
または SAML もしくは Open ID Connect を介して外部ディレクトリの認証情報を使用して、サインインできます。ユーザープールのフェデレーティッドユーザーのユーザー属性マッピングとセキュリティを管理できます。
今回、saml,oidcをやりたいのでそうした
電話番号はなんか好きじゃない
パスワードポリシー
→ 有効期限がデフォルトだと7日だが、3日にした。なんか長い。
多要素認証
- MFA必須だとユーザーが離脱しそうなので一旦やめた
- Authenticatorアプリケーションはよく使うので選択した
- smsメッセージもよくあるパターンなので選択した
- ユーザーアカウントの復旧については使用できる場合はEメール、それ以外の場合はSMSにした
サインアップエクスペリエンス
- 自己登録は有効化しない BtoB saasでそれはやっちゃいけないと思ってる。
- 属性検証とユーザーアカウントの確認は画面の通り。と言うかデフォルトを利用した。
- カスタム属性でtenantを追加
Eメール送信については、プロダクションではないのでやめておいた
smsのロールはわかりやすい名前で新規作成した
cognitoのクライアントを使用せずにカスタムしてできるよう作ってみる
ココ迷うな。
クライアントシークレットは作成するけど。
ここら辺は改めて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のボディ
↓ 参照元
{
"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になる。(と思ってる)
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を用いて共有鍵を生成し、それを用いてセッションキーを生成する。
クライアントはユーザー名、パスワード、共有鍵、ソルトを用いてセッションキーを生成し、それをハッシュ化したものをサーバに送信する。
サーバーは検証子、ソルト、共有鍵を用いてセッションキーを生成する。
最後に、サーバーでセッションキーが一致するか確認し、検証結果をクライアントに送信する。
ーーーーこれを写経しただけーーーー