はじめに
SESエンジニアをしております。masahiroと申します。
本記事では、APIサーバーからAWSの認証サービスCognitoへの接続方法について記述しております。
この記事での実装機能を構築するには、AWSコンソール内でのCognitoユーザープールの作成が必要です。
作成されていない方は下記の記事を参照してください。
AWS Cognitoでのユーザープールの設定方法
実装
まずはAPIエンドポイントの実装から見ていきます。
APIエンドポイントの実装
gorilla/muxの導入
ルーティング機能として、以下のWebサーバーフレームワークをインストールします。
go install github.com/gorilla/mux
インストール後は以下のように設定を行います。
package main
import (
"fmt"
"github.com/MasahiroYoshiichi/auth/cognito/cognitotoken"
"github.com/MasahiroYoshiichi/auth/cognito/handlers"
"github.com/gorilla/mux"
"log"
"net/http"
)
func main() {
// ルーター設定(gorilla使用)
router := mux.NewRouter()
// APIエンドポイント
router.HandleFunc("/signup", handlers.SignUpHandler).Methods("POST")
router.HandleFunc("/confirm-signup", handlers.ConfirmSignUpHandler).Methods("POST")
router.HandleFunc("/signin", handlers.SignInHandler).Methods("POST")
router.HandleFunc("/mfa", handlers.MFAHandler).Methods("POST")
router.Handle("/signout", cognitotoken.Middleware(http.HandlerFunc(handlers.SignOutHandler))).Methods("POST")
// APIサーバー設定
port := ":8080"
fmt.Printf("API server started at %s\n", port)
log.Fatal(http.ListenAndServe(port, router))
}
上記では、各Apiエンドポイントのパスに紐づく処理とApiサーバの立ち上げを実施しています。
また、サインアウトなどの一部の実装には、JWT認証トークンを利用したミドルウェアを設定しています。
こちらは別途下記で解説をしておりますので、興味があれば見ていただけたらと思います。
AWS接続時の設定ファイルの作成
ルートディレクトリと同じ階層にConfigディレクトリを作成し、以下の2つのファイルを作成してください。
・config.go
・config.json
config.jsonの実装
こちらには、AWS Cognitoの設定情報を入力していきます。
{
"aws_region": "ap-northeast-1",
"user_pool_id": "ap-northeast-xxxxxxxxx",
"client_id": "xxxxxxxxxxxxxxxxxxxxxxxxx",
}
この設定の確認方法としては、下記の記事を参考にしてください。
config.goの実装
package config
import (
"encoding/json"
"os"
)
// 設定情報を
type Config struct {
AwsRegion string `json:"aws_region"`
UserPoolId string `json:"user_pool_id"`
ClientId string `json:"client_id"`
}
func LoadConfig() (*Config, error) {
file, err := os.Open("/usr/local/bin/config.json")
if err != nil {
return nil, err
}
defer file.Close()
decoder := json.NewDecoder(file)
config := &Config{}
err = decoder.Decode(config)
if err != nil {
return nil, err
}
return config, nil
}
上記の実装で、jsonに設定した内容を取得し、構造体のタグに基づいてデコードを実施しています。
サインアップ機能を実装
上記でAWSとの接続情報に関しては、設定ができたので、
サインアップ機能の実装に取り組んでいきます。
SignUpハンドラの作成
まず、ルートディレクトリにhandlersパッケージを作成を作成し、
signup.goファイルを作成します。
/handlers/signUp.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 SignUpHandler(w http.ResponseWriter, r *http.Request) {
// AWS設定ファイル読み込み
cfg, err := config.LoadConfig()
if err != nil {
log.Printf("設定ファイルの読み込みに失敗しました。: %v\n", err)
http.Error(w, "サーバー内部エラー:"+err.Error(), http.StatusInternalServerError)
return
}
// リクエスト処理
var signupInfo models.SignUpInfo
err = json.NewDecoder(r.Body).Decode(&signupInfo)
if err != nil {
log.Printf("リクエストボディの読み込みに失敗しました。: %v\n", err)
http.Error(w, "不正なリクエスト:"+err.Error(), http.StatusBadRequest)
return
}
// AWS認証情報を元にServiceを作成
signUpService := services.NewSignUpService(cfg)
// AWSCognitoサインアップ
_, err = signUpService.SignUp(signupInfo)
if err != nil {
log.Printf("登録に失敗しました。: %v\n", err)
http.Error(w, "サーバー内部エラー:"+err.Error(), http.StatusInternalServerError)
return
}
// username Cookie 格納
http.SetCookie(w, &http.Cookie{
Name: "username",
Value: signupInfo.Username,
HttpOnly: true,
Secure: false,
SameSite: http.SameSiteStrictMode, // Change this to None
Expires: time.Now().Add(30 * time.Minute),
})
log.Printf("Cookieを設定(username): %s\n", signupInfo.Username)
// httpステータス返却
w.WriteHeader(http.StatusOK)
}
このコードでは、AWSの設定情報を元にサービスを作成し、その中のSignUpメソッドでユーザー登録を実施しています。
コード後半で記載している、Cookieの設定ですが、こちらはサインアップ検証時に必要になります。
今回はHTTPOnlyCookieを利用して、セキュアにクライアントサイドにCookieを返却しています。
細かな処理に関してはコメントに記載しているため、ここでは省略させていただきます。
SignUpサービスの作成
実際に登録処理をするサービスについて実装していきます。
こちらもルートディレクトリにservicesディレクトリを作成し、signUpService.goを作成してください。
/services/signUpServiceを作成してください。
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"
)
// SignUpService サービスの構造体を作成
type SignUpService struct {
cognitoClient *cognitoidentityprovider.CognitoIdentityProvider
clientId string
}
// NewSignUpService サインアップのサービスを作成(引数:AWS設定情報、戻り値:サービスの構造体)
func NewSignUpService(cfg *config.Config) *SignUpService {
// AWSリージョンとのセッションを確保
sess := session.Must(session.NewSession(&aws.Config{Region: aws.String(cfg.AwsRegion)}))
// Cognitoのクライアントを作成
cognitoClient := cognitoidentityprovider.New(sess)
// サービス構造体を作成
return &SignUpService{
cognitoClient: cognitoClient,
clientId: cfg.ClientId,
}
}
// SignUp Cognitoへのサインアップを実行(引数;認証情報、戻り値:Cognitoクライアントへの登録情報)
func (s *SignUpService) SignUp(signupInfo models.SignUpInfo) (*cognitoidentityprovider.SignUpOutput, error) {
// サービスで作成したCognitoクライアントへ登録情報を格納
signUpInput := &cognitoidentityprovider.SignUpInput{
// AWS設定情報のクライアントID
ClientId: aws.String(s.clientId),
// 登録情報(必須)
Username: aws.String(signupInfo.Username),
Password: aws.String(signupInfo.Password),
//登録情報(追加属性)
UserAttributes: []*cognitoidentityprovider.AttributeType{
{
Name: aws.String("email"),
Value: aws.String(signupInfo.Email),
},
{
Name: aws.String("phone_number"),
Value: aws.String(signupInfo.PhoneNumber),
},
},
}
//aws.Stringはawsのポインタ型に対して文字列をマッピングするためのもの
// Cognitoクライアントへの登録を実行
return s.cognitoClient.SignUp(signUpInput)
}
上記のコードでは、ハンドラから渡されたAWSの設定情報を元に、
Cognitoとのクライアントを作成し、サービス構造体に格納します。
その後に、構造体をレシーバとして受け取り、メソッドに渡された登録情報を元にCognitoへの登録を実施します。
今回の設定では、サインアップ時の検証、MFA認証のために必須情報以外も登録しています。
必要に応じて登録情報は変更してください。
ここまでで、Cognitoを利用したユーザーの一時登録が完了しました。
次で、登録情報を検証し、認証情報として利用できるようにしていきます。
サインアップ検証機能の実装
こちらでは、登録時に使用したメールアドレスに送信される検証情報を元に、検証機能を実装していきます。
CoinfirmSignUpハンドラの作成
前回と同じくhandlersディレクトリにconfirmSignUp.goを作成します。
/handlers/confirumSignUpを作成してください。
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 ConfirmSignUpHandler(w http.ResponseWriter, r *http.Request) {
// AWS設定ファイル読み込み
cfg, err := config.LoadConfig()
if err != nil {
log.Printf("設定ファイルの読み込みに失敗しました。: %v\n", err)
http.Error(w, "サーバー内部エラー: "+err.Error(), http.StatusInternalServerError)
return
}
// username Cookie 取得
var confirmUser string
usernameCookie, err := r.Cookie("username")
if err != nil {
log.Printf("UsernameのCookieが取得できませんでした。: %v\n", err)
http.Error(w, "サーバー内部エラー: "+err.Error(), http.StatusBadRequest)
return
}
// username Cookie 変数へ格納
confirmUser = usernameCookie.Value
log.Printf("UsernameのCookieを表示します。: %s\n", usernameCookie)
// リクエスト処理
var confirmCode models.ConfirmCode
err = json.NewDecoder(r.Body).Decode(&confirmCode)
if err != nil {
log.Printf("リクエストボディの読み込みに失敗しました: %v\n", err)
http.Error(w, "不正なリクエスト: "+err.Error(), http.StatusBadRequest)
return
}
// AWS認証情報を元にServiceを作成
confirmSignUpService := services.NewConfirmSignUpService(cfg)
// AWSCognitoサインアップ検証
err = confirmSignUpService.ConfirmSignUp(confirmUser, confirmCode)
if err != nil {
log.Printf("検証に失敗しました: %s\n", err)
http.Error(w, "サーバー内部エラー: "+err.Error(), http.StatusInternalServerError)
return
}
// httpステータス返却
w.WriteHeader(http.StatusOK)
}
上記のコードでは、サインアップ時と同様に、AWS設定情報でサービスを作成し、
リクエストの検証コードとサインアップ時のCookieの中身からusernameを取得して、
confirumSignUpメソッドに渡しています。
ConfirumSignUpサービスの作成
検証コード及び登録時のユーザーネームを元にサインアップの検証を実装します。
/services/confirumSignUpServiceを作成してください。
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"
)
// ConfirmSignUpService サービスの構造体を作成
type ConfirmSignUpService struct {
cognitoClient *cognitoidentityprovider.CognitoIdentityProvider
clientId string
}
// NewConfirmSignUpService サインアップ検証のサービスを作成(引数:AWS設定情報、戻り値:サービスの構造体)
func NewConfirmSignUpService(cfg *config.Config) *ConfirmSignUpService {
// AWSリージョンとのセッションを確保
sess := session.Must(session.NewSession(&aws.Config{Region: aws.String(cfg.AwsRegion)}))
// Cognitoのクライアントを作成
cognitoClient := cognitoidentityprovider.New(sess)
// サービス構造体を作成
return &ConfirmSignUpService{
cognitoClient: cognitoClient,
clientId: cfg.ClientId,
}
}
// ConfirmSignUp Cognitoへのサインアップ検証を実行(引数;認証情報)
func (s *ConfirmSignUpService) ConfirmSignUp(confirmUser string, confirmCode models.ConfirmCode) error {
// サービスで作成したCognitoクライアントへ検証情報を格納
confirmSignInInput := &cognitoidentityprovider.ConfirmSignUpInput{
// AWS設定情報のクライアントID
ClientId: aws.String(s.clientId),
// 検証情報
Username: aws.String(confirmUser),
ConfirmationCode: aws.String(confirmCode.ConfirmationCode),
}
// Cognitoクライアントへの検証を実行
_, err := s.cognitoClient.ConfirmSignUp(confirmSignInInput)
return err
}
こちらの処理はサインアップ時の処理とほとんど変わらず、
設定したCognitoクライアントとの間で、メソッドに渡された検証情報と登録ユーザー名を元に検証を実施しています。
おわりに
ここまででサインアップとサインアップ時の検証に関する実装は完了しました。
サインイン時とサインアウト時の実装については下記の記事を参照してください。
【Go】AwsCognitoを利用した認証機能を作ってみた(後編)