1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Go】AwsCognitoを利用した認証機能を作ってみた(後編)

Last updated at Posted at 2023-05-19

はじめに

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を検索してください。

スクリーンショット 2023-05-19 18.11.46.png

次にメニューからテキストメッセージを選択してください。
スクリーンショット 2023-05-19 18.13.13.png

cognitoからSNSへの連携機能を使用するためには、SMSサンドボックスを終了させる必要があります。

「SMSサンドボックス終了」ボタンを押下

スクリーンショット 2023-05-19 18.16.04.png

サービス制限の緩和申請

下記の内容を適宜変更して入力

入力欄は全て埋めていないと申請が通らないので注意してください。

スクリーンショット 2023-05-19 18.20.28.png

リクエスト内容を入力してください。

今回は2要素認証として申請しています。

スクリーンショット 2023-05-19 18.23.02.png

最後にケースの説明をします。
例文として下記のように申請をします。

スクリーンショット 2023-05-19 18.25.02.png

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認証機能の構築手順となります。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?