はじめに
SESエンジニアをしております。masahiroと申します。
本記事では、APIサーバーからAWSの認証サービスCognitoへの接続方法について記述しております。
この記事での実装機能を構築するには、AWSコンソール内でのCognitoユーザープールの作成が必要です。
作成されていない方は下記の記事を参照してください。
AWS Cognitoでのユーザープールの設定方法
前編では、サインアップとサインアップ時の検証機能を構築しているので、そちらから参照してください。
【Go】AwsCognitoを利用した認証機能を作ってみた(前編)
実装
まずは、サインイン時の実装をしていきます。
サインイン機能
今回は、サインアップ時に登録したメールアドレスとパスワードを使用して認証する実装になります。
SignInハンドラの作成
/handlers/signIn.goを作成してください。
package handlers
import (
"encoding/json"
"github.com/MasahiroYoshiichi/auth/cognito/models"
"github.com/MasahiroYoshiichi/auth/cognito/services"
"github.com/MasahiroYoshiichi/auth/config"
"log"
"net/http"
"time"
)
func SignInHandler(w http.ResponseWriter, r *http.Request) {
// AWS設定ファイル読み込み
cfg, err := config.LoadConfig()
if err != nil {
log.Println("設定ファイルの読み込みに失敗しました。:", err)
http.Error(w, "サーバー内部エラー", http.StatusInternalServerError)
return
}
// リクエスト処理
var signinInfo models.SignInInfo
err = json.NewDecoder(r.Body).Decode(&signinInfo)
if err != nil {
log.Printf("リクエストボディの読み込みに失敗しました: %v\n", err)
http.Error(w, "不正なリクエスト", http.StatusBadRequest)
return
}
// AWS認証情報を元にServiceを作成
signInService := services.NewSignInService(cfg)
// AWSCognitoサインイン
initiateAuthOutput, err := signInService.SignIn(signinInfo)
if err != nil {
log.Printf("認証に失敗しました。: %v\n", err)
http.Error(w, "サーバー内部エラー:"+err.Error(), http.StatusInternalServerError)
return
}
// email Cookie 格納
http.SetCookie(w, &http.Cookie{
Name: "email",
Value: signinInfo.Email,
HttpOnly: true,
Secure: false,
SameSite: http.SameSiteStrictMode,
Expires: time.Now().Add(30 * time.Minute),
})
log.Printf("Cookieを設定(email): %s\n", signinInfo.Email)
// Session Cookie 格納
http.SetCookie(w, &http.Cookie{
Name: "session",
Value: *initiateAuthOutput.Session,
HttpOnly: true,
Secure: false,
SameSite: http.SameSiteStrictMode,
Expires: time.Now().Add(30 * time.Minute),
})
log.Printf("Cookieを設定(session): %s\n", *initiateAuthOutput.Session)
// httpステータス返却
w.WriteHeader(http.StatusOK)
}
上記のコードでは、リクエストのメールアドレスとパスワードをサービスのメソッドに渡すといった、基本的な実装をしています。
コードの終盤では、HTTPOnlyCookieを利用して、値を返却しています。
Cookieに格納されている、emailとsessionはMFA認証時に必要な情報になります。
SignInServiceの作成
/services/signInService.goを作成してください。
package services
import (
"github.com/MasahiroYoshiichi/auth/cognito/models"
"github.com/MasahiroYoshiichi/auth/config"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/cognitoidentityprovider"
)
// SignInService サービスの構造体を作成
type SignInService struct {
cognitoClient *cognitoidentityprovider.CognitoIdentityProvider
clientId string
}
// NewSignInService サインインのサービスを作成(引数:AWS設定情報、戻り値:サービスの構造体)
func NewSignInService(cfg *config.Config) *SignInService {
// AWSリージョンとのセッションを確保
sess := session.Must(session.NewSession(&aws.Config{Region: aws.String(cfg.AwsRegion)}))
// Cognitoのクライアントを作成
cognitoClient := cognitoidentityprovider.New(sess)
// サービス構造体を作成
return &SignInService{
cognitoClient: cognitoClient,
clientId: cfg.ClientId,
}
}
// SignIn Cognitoへのサインアップを実行(引数;認証情報、戻り値:セッション情報)
func (s *SignInService) SignIn(signinInfo models.SignInInfo) (*cognitoidentityprovider.InitiateAuthOutput, error) {
// サービスで作成したCognitoクライアントへ認証情報を格納
input := &cognitoidentityprovider.InitiateAuthInput{
// AWS設定情報のクライアントID
ClientId: aws.String(s.clientId),
// 認証情報
AuthFlow: aws.String(cognitoidentityprovider.AuthFlowTypeUserPasswordAuth),
AuthParameters: map[string]*string{
"USERNAME": aws.String(signinInfo.Email),
"PASSWORD": aws.String(signinInfo.Password),
},
}
// Cognitoクライアントへの認証を実行
initiateAuthOutput, err := s.cognitoClient.InitiateAuth(input)
if err != nil {
return nil, err
}
if initiateAuthOutput.ChallengeName != nil && *initiateAuthOutput.ChallengeName == cognitoidentityprovider.ChallengeNameTypeSmsMfa {
return initiateAuthOutput, nil
}
// MFA認証時に必要なセッション情報を格納
return initiateAuthOutput, nil
}
上記コードでは、cognitoクライアントを設定している構造体を利用したメソッドで、
メールアドレスとパスワードを使用して認証を実施しています。
MFA認証機能
サインイン時にCognitoではAWS SNS機能を利用して、ショートメッセージを送るように設計されていますが、
この機能を利用するには、別途SNS機能を設定しなければいけません。
AWS SNSの設定
まず、AWSマネージメントコンソールにログインします。
左上の検索欄からSNSを検索してください。
cognitoからSNSへの連携機能を使用するためには、SMSサンドボックスを終了させる必要があります。
「SMSサンドボックス終了」ボタンを押下
サービス制限の緩和申請
下記の内容を適宜変更して入力
入力欄は全て埋めていないと申請が通らないので注意してください。
リクエスト内容を入力してください。
今回は2要素認証として申請しています。
最後にケースの説明をします。
例文として下記のように申請をします。
MFA認証機能
AWS SNSの設定が完了したので、MFA認証機能の実装をしていきます。
MFAハンドラを作成
/handlers/mfaSignIn.goを作成
package handlers
import (
"encoding/json"
"github.com/MasahiroYoshiichi/auth/cognito/models"
"github.com/MasahiroYoshiichi/auth/cognito/services"
"github.com/MasahiroYoshiichi/auth/config"
"log"
"net/http"
)
func MFAHandler(w http.ResponseWriter, r *http.Request) {
// AWS設定ファイル読み込み
cfg, err := config.LoadConfig()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
//email Cookie 取得
var mfaEmail string
emailCookie, err := r.Cookie("email")
if err != nil {
http.Error(w, "EmailのCookieが取得できませんでした。", http.StatusBadRequest)
}
mfaEmail = emailCookie.Value
log.Printf("email Cookie: %s\n", mfaEmail)
//session Cookie 取得
var mfaSession string
sessionCookie, err := r.Cookie("session")
if err != nil {
http.Error(w, "SessionのCookieが取得できませんでした。", http.StatusBadRequest)
}
mfaSession = sessionCookie.Value
log.Printf("session Cookie: %s\n", mfaSession)
//MFACode 取得
var mfaCode models.MFACode
err = json.NewDecoder(r.Body).Decode(&mfaCode)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// AWS認証情報を元にServiceを作成
MFASignInService := services.NewMFASignInService(cfg)
// AWSCognitoMFA認証を実施
authenticationResult, err := MFASignInService.CompleteMFA(mfaSession, mfaEmail, mfaCode)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// アクセストークンと認証状態管理トークンを返す
response := struct {
Token string `json:"accessToken"`
Authentication string `json:"authentication"`
}{
Token: *authenticationResult.IdToken,
Authentication: "true",
}
json.NewEncoder(w).Encode(response)
}
emailとsessionをCookieから取得、リクエストのMFA認証コードと一緒にMFA認証サービスに渡します。
処理が完了すると、cognito側からアクセストークンが帰ってくるので、これを元に認証後にユーザーを判定して処理を実行するようにします。
また、ブラウザ側での認証状態を管理するために、認証状態管理するトークンを返却しブラウザでの状態管理で使用します。
MFAサービスを作成
/services/mfaSignInServiceを作成
package services
import (
"github.com/MasahiroYoshiichi/auth/cognito/models"
"github.com/MasahiroYoshiichi/auth/config"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/cognitoidentityprovider"
)
// MFASignInService サービスの構造体を作成
type MFASignInService struct {
cognitoClient *cognitoidentityprovider.CognitoIdentityProvider
clientId string
}
// NewMFASignInService MFA認証のサービスを作成(引数:AWS設定情報、戻り値:サービスの構造体)
func NewMFASignInService(cfg *config.Config) *SignInService {
// AWSリージョンとのセッションを確保
sess := session.Must(session.NewSession(&aws.Config{Region: aws.String(cfg.AwsRegion)}))
// Cognitoのクライアントを作成
cognitoClient := cognitoidentityprovider.New(sess)
// サービス構造体を作成
return &SignInService{
cognitoClient: cognitoClient,
clientId: cfg.ClientId,
}
}
// CompleteMFA SignIn CognitoへのMFA認証を実行(引数;認証情報、戻り値:アクセストークン)
func (s *SignInService) CompleteMFA(mfaSession string, mfaEmail string, mfaCode models.MFACode) (*cognitoidentityprovider.AuthenticationResultType, error) {
// サービスで作成したCognitoクライアントへ認証情報を格納
input := &cognitoidentityprovider.RespondToAuthChallengeInput{
// AWS設定情報のクライアントID
ClientId: aws.String(s.clientId),
// 認証情報
ChallengeName: aws.String(cognitoidentityprovider.ChallengeNameTypeSmsMfa),
Session: aws.String(mfaSession),
ChallengeResponses: map[string]*string{
"USERNAME": aws.String(mfaEmail),
"SMS_MFA_CODE": aws.String(mfaCode.MFACode),
},
}
// CognitoクライアントへのMFA認証を実行
res, err := s.cognitoClient.RespondToAuthChallenge(input)
if err != nil {
return nil, err
}
// アクセストークンなどの認証情報を返す
return res.AuthenticationResult, nil
}
クッキーとリクエストから取得した認証情報を利用して、cognitoへのMFA認証を実施します。
正常に実施できた場合には、アクセストークンをクラインアントへ返すようになっています。
返却されるアクセストークンは、jwt形式でミドルウェアを介してcognitoとの認証に使用されます。
おわりに
以上がcognitoを使用した、サインイン機能とMFA認証機能の構築手順となります。